commit e48adbcb00596be2523c36e6f7661e4853d4b2e7 Author: William Valentin Date: Sat Sep 6 01:42:48 2025 -0700 Initial commit: Complete NodeJS-native setup - Migrated from Python pre-commit to NodeJS-native solution - Reorganized documentation structure - Set up Husky + lint-staged for efficient pre-commit hooks - Fixed Dockerfile healthcheck issue - Added comprehensive documentation index diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..546c4d0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# EditorConfig helps maintain consistent coding styles +# See https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.{js,jsx,ts,tsx,json,css,scss,html,vue}] +indent_style = space +indent_size = 2 + +[*.{md,markdown}] +trim_trailing_whitespace = false + +[*.{py}] +indent_size = 4 + +[*.{go}] +indent_style = tab + +[Makefile] +indent_style = tab + +[*.{yml,yaml}] +indent_size = 2 + +[*.{sh,bash}] +indent_size = 2 + +[Dockerfile*] +indent_size = 2 diff --git a/.env.demo b/.env.demo new file mode 100644 index 0000000..fbb1052 --- /dev/null +++ b/.env.demo @@ -0,0 +1,25 @@ +# Demo Environment Configuration for RxMinder +# This demonstrates the template-based approach + +# Application Configuration +APP_NAME=my-rxminder +INGRESS_HOST=rxminder.demo.local + +# Docker Image Configuration +DOCKER_IMAGE=my-registry.com/rxminder:demo + +# Database Configuration (no base64 encoding needed!) +COUCHDB_USER=admin +COUCHDB_PASSWORD=super-secure-demo-password-123 + +# Storage Configuration (NEW!) +STORAGE_CLASS=fast-ssd +STORAGE_SIZE=10Gi + +# Frontend Configuration +VITE_COUCHDB_URL=http://localhost:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=super-secure-demo-password-123 + +# Application Settings +APP_BASE_URL=http://rxminder.demo.local diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ce76263 --- /dev/null +++ b/.env.example @@ -0,0 +1,88 @@ +# Environment Configuration Template +# Copy this file to .env and fill in your actual values +# DO NOT commit .env to version control + +# Application Name (used in Kubernetes labels and branding) +APP_NAME=rxminder + +# Docker Image Configuration +# Examples: +# - Local registry: localhost:5000/rxminder:latest +# - Docker Hub: rxminder/rxminder:v1.0.0 +# - GitHub Container Registry: ghcr.io/username/rxminder:latest +# - AWS ECR: 123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:latest +DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:latest + +# CouchDB Configuration +COUCHDB_USER=admin +COUCHDB_PASSWORD=change-this-secure-password +VITE_COUCHDB_URL=http://localhost:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=change-this-secure-password + +# Application Configuration +# Base URL for your application (used in email links) +# Development: http://localhost:5173 +# Production: https://your-domain.com +APP_BASE_URL=http://localhost:5173 + +# Kubernetes Ingress Configuration +# Host for Kubernetes ingress (used in deployment) +# Examples: app.rxminder.192.168.1.100.nip.io, rxminder.yourdomain.com +INGRESS_HOST=app.rxminder.192.168.1.100.nip.io + +# Kubernetes Storage Configuration +# Storage class for PersistentVolumeClaims +# Common options: longhorn, local-path, standard, gp2, fast-ssd +STORAGE_CLASS=longhorn + +# Storage size for CouchDB data +# Examples: 1Gi, 5Gi, 10Gi, 100Gi +STORAGE_SIZE=5Gi + +# Mailgun Email Configuration +MAILGUN_API_KEY=your-mailgun-api-key-here +MAILGUN_DOMAIN=your-domain.com +MAILGUN_FROM_EMAIL=noreply@your-domain.com + +# Production-specific settings +NODE_ENV=development + +# Optional: External CouchDB for production +# VITE_COUCHDB_URL=https://your-couchdb-instance.com:5984 +# VITE_COUCHDB_USER=production-user +# VITE_COUCHDB_PASSWORD=super-secure-production-password + +# OAuth Configuration (Optional - for production OAuth) +VITE_GOOGLE_CLIENT_ID=your_google_client_id_here +VITE_GITHUB_CLIENT_ID=your_github_client_id_here + +# ============================================================================ +# CONTAINER REGISTRY CONFIGURATION +# ============================================================================ + +# Container registry for Docker images +# Examples: +# - GitHub Container Registry: ghcr.io +# - GitLab Container Registry: registry.gitlab.com +# - Gitea Container Registry: gitea.yourdomain.com +# - Docker Hub: docker.io (or leave empty) +CONTAINER_REGISTRY=ghcr.io + +# Repository name for container images +# Format: username/repository-name or organization/repository-name +CONTAINER_REPOSITORY=yourusername/rxminder + +# Gitea-specific settings +# Repository name in Gitea (alternative to CONTAINER_REPOSITORY) +GITEA_REPOSITORY=yourusername/rxminder + +# ============================================================================ +# CI/CD CONFIGURATION +# ============================================================================ + +# Deployment webhook URL for notifications (optional) +DEPLOYMENT_WEBHOOK_URL= + +# Image cleanup settings +CLEANUP_OLD_IMAGES=true \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..7efb677 --- /dev/null +++ b/.env.production @@ -0,0 +1,38 @@ +# Production environment configuration +# Application Name (used in Kubernetes labels and branding) +APP_NAME=rxminder + +# Docker Image Configuration +# Use a specific tag for production (not :latest) +DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:v1.2.0 + +# CouchDB Configuration +COUCHDB_USER=admin +COUCHDB_PASSWORD=change-this-secure-password +VITE_COUCHDB_URL=http://localhost:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=change-this-secure-password + +# Application configuration +APP_BASE_URL=https://your-production-domain.com + +# Kubernetes Ingress Configuration +INGRESS_HOST=meds.your-production-domain.com + +# Kubernetes Storage Configuration +# Production storage class (adjust for your cluster) +STORAGE_CLASS=fast-ssd +# Production storage size (larger for production data) +STORAGE_SIZE=20Gi + +# Mailgun configuration for production +MAILGUN_API_KEY=your-production-mailgun-api-key-here +MAILGUN_DOMAIN=your-production-domain.com +MAILGUN_FROM_EMAIL=noreply@your-production-domain.com + +# Production-specific settings +NODE_ENV=production + +# OAuth Configuration (Optional) +VITE_GOOGLE_CLIENT_ID=your_google_client_id_here +VITE_GITHUB_CLIENT_ID=your_github_client_id_here diff --git a/.gitea/README.md b/.gitea/README.md new file mode 100644 index 0000000..021bb06 --- /dev/null +++ b/.gitea/README.md @@ -0,0 +1,236 @@ +# Gitea Actions Configuration for RxMinder + +## Environment Variables + +### Required Secrets (Configure in Gitea Repository Settings) + +``` +GITEA_TOKEN # Gitea access token for registry access +VITE_COUCHDB_PASSWORD # CouchDB password (sensitive) +DEPLOYMENT_WEBHOOK_URL # Optional: webhook for deployment notifications +``` + +### Repository Variables (Configure in Gitea Repository Settings) + +``` +VITE_COUCHDB_URL # Default: http://localhost:5984 +VITE_COUCHDB_USER # Default: admin +APP_BASE_URL # Default: http://localhost:8080 +VITE_GOOGLE_CLIENT_ID # Optional: Google OAuth client ID +VITE_GITHUB_CLIENT_ID # Optional: GitHub OAuth client ID +GITEA_REGISTRY # Container registry URL (e.g., gitea.yourdomain.com) +GITEA_REPOSITORY # Repository name (e.g., username/rxminder) +``` + +### Environment Variables (.env file) + +The scripts will automatically load configuration from your `.env` file. Copy `.env.example` to `.env` and customize: + +```bash +# Copy example and customize +cp .env.example .env + +# Key variables for container registry: +CONTAINER_REGISTRY=gitea.yourdomain.com +CONTAINER_REPOSITORY=username/rxminder +GITEA_REGISTRY=gitea.yourdomain.com # Alternative to CONTAINER_REGISTRY +GITEA_REPOSITORY=username/rxminder # Alternative to CONTAINER_REPOSITORY +``` + +## Gitea Actions Features + +### Workflows + +- **Build & Test**: Multi-platform Docker builds with buildx +- **Security Scanning**: Trivy vulnerability scanning +- **Deployment**: Automated deployment to production +- **Cleanup**: Registry and image cleanup + +### Multi-Platform Support + +- linux/amd64 (Intel/AMD) +- linux/arm64 (ARM64/Apple Silicon) + +### Caching Strategy + +- Registry-based caching for faster builds +- Layer caching between builds +- Dependency caching for Node.js/Bun + +## Setup Instructions + +### 1. Gitea Server Requirements + +```bash +# Minimum Gitea version +Gitea >= 1.20.0 with Actions enabled + +# Required Gitea features +- Gitea Actions enabled +- Container Registry enabled +- Runners configured +``` + +### 2. Configure Gitea Runner + +```yaml +# .gitea/runners/config.yml (on runner machine) +name: 'rxminder-runner' +labels: + - 'ubuntu-latest' + - 'self-hosted' +capabilities: + - docker + - buildx +``` + +### 3. Repository Configuration + +```bash +# 1. Go to Repository Settings → Actions → Secrets +# Add required secrets and variables + +# 2. Go to Repository Settings → Packages +# Enable container registry + +# 3. Configure runner labels in workflow files if needed +``` + +### 4. Local Testing + +```bash +# Test Gitea Actions locally with act +# Install: https://github.com/nektos/act + +# Test the workflow +act -P ubuntu-latest=catthehacker/ubuntu:act-latest + +# Test specific job +act -P ubuntu-latest=catthehacker/ubuntu:act-latest -j build +``` + +## Deployment Targets + +### Docker Compose (Default) + +```bash +# Deploys using docker-compose.yml +# Suitable for single-server deployments +./scripts/gitea-deploy.sh production +``` + +### Kubernetes + +```bash +# Deploys to Kubernetes cluster +# Requires kubectl configured +./scripts/gitea-deploy.sh kubernetes +``` + +### Staging Environment + +```bash +# Deploys to staging with different configs +./scripts/gitea-deploy.sh staging +``` + +## Monitoring & Notifications + +### Health Checks + +- Frontend: `http://localhost:8080/health` +- CouchDB: `http://localhost:5984/_up` + +### Deployment Notifications + +Configure `DEPLOYMENT_WEBHOOK_URL` to receive notifications: + +```json +{ + "text": "✅ RxMinder deployed to production", + "environment": "production", + "image": "gitea.example.com/user/rxminder:abc123" +} +``` + +## Troubleshooting + +### Common Issues + +1. **Build Fails - Buildx Not Available** + + ```bash + # Ensure Docker Buildx is installed on runner + docker buildx version + ``` + +2. **Registry Push Fails** + + ```bash + # Check GITEA_TOKEN has package write permissions + # Verify registry URL is correct + ``` + +3. **Deployment Fails** + ```bash + # Check environment variables are set + # Verify server has Docker/Kubernetes access + ``` + +### Debug Commands + +```bash +# Check workflow logs in Gitea UI +# Repository → Actions → [Workflow Run] + +# Test deployment script locally +./scripts/gitea-deploy.sh production --debug + +# Check service status +docker-compose -f docker/docker-compose.yaml ps +docker-compose -f docker/docker-compose.yaml logs +``` + +## Security Considerations + +### Image Scanning + +- Trivy vulnerability scanning in CI +- Base image security updates +- Dependency audit checks + +### Secrets Management + +- Use Gitea secrets for sensitive data +- Rotate access tokens regularly +- Limit token permissions + +### Registry Security + +- Private registry recommended +- Image signing (optional) +- Regular image cleanup + +## Performance Optimization + +### Build Optimization + +- Multi-stage Dockerfile +- Layer caching +- Minimal base images + +### Deployment Optimization + +- Health checks +- Rolling updates +- Resource limits + +## Migration from GitHub Actions + +If migrating from GitHub Actions: + +1. **Copy workflow structure** (already compatible) +2. **Update variable references**: `github.` → `gitea.` +3. **Configure secrets** in Gitea repository settings +4. **Test locally** with act before pushing +5. **Update registry URLs** if different diff --git a/.gitea/docker-compose.ci.yml b/.gitea/docker-compose.ci.yml new file mode 100644 index 0000000..34caa9a --- /dev/null +++ b/.gitea/docker-compose.ci.yml @@ -0,0 +1,45 @@ +# Gitea Actions CI/CD Docker Compose Override +# This file provides CI-specific configurations for Gitea Actions + +version: '3.8' + +services: + # Frontend service with CI optimizations + frontend: + build: + context: . + target: builder + cache_from: + - ${REGISTRY:-gitea.example.com}/${IMAGE_NAME:-rxminder}:buildcache + args: + # Use build args from CI environment + - VITE_COUCHDB_URL=${VITE_COUCHDB_URL:-http://couchdb:5984} + - VITE_COUCHDB_USER=${VITE_COUCHDB_USER:-admin} + - VITE_COUCHDB_PASSWORD=${VITE_COUCHDB_PASSWORD:-change-this-secure-password} + - APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080} + - VITE_GOOGLE_CLIENT_ID=${VITE_GOOGLE_CLIENT_ID:-} + - VITE_GITHUB_CLIENT_ID=${VITE_GITHUB_CLIENT_ID:-} + - NODE_ENV=production + environment: + - CI=true + labels: + - 'gitea.ci=true' + - 'gitea.project=rxminder' + + # Test database for CI + couchdb-test: + image: couchdb:3.3.2 + environment: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=test-secure-password + ports: + - '5985:5984' + volumes: + - couchdb_test_data:/opt/couchdb/data + labels: + - 'gitea.ci=true' + - 'gitea.service=test-database' + +volumes: + couchdb_test_data: + driver: local diff --git a/.gitea/gitea-bake.hcl b/.gitea/gitea-bake.hcl new file mode 100644 index 0000000..d2a7e70 --- /dev/null +++ b/.gitea/gitea-bake.hcl @@ -0,0 +1,156 @@ +# Gitea-specific Docker Bake file for advanced multi-platform builds +# Usage: docker buildx bake -f gitea-bake.hcl + +variable "GITEA_REGISTRY" { + default = notequal("", GITEA_REGISTRY) ? GITEA_REGISTRY : "ghcr.io" +} + +variable "GITEA_REPOSITORY" { + default = notequal("", GITEA_REPOSITORY) ? GITEA_REPOSITORY : "user/rxminder" +} + +variable "TAG" { + default = "latest" +} + +variable "GITEA_SHA" { + default = "dev" +} + +variable "VITE_COUCHDB_URL" { + default = "http://localhost:5984" +} + +variable "VITE_COUCHDB_USER" { + default = "admin" +} + +variable "VITE_COUCHDB_PASSWORD" { + default = "change-this-secure-password" +} + +variable "APP_BASE_URL" { + default = "http://localhost:8080" +} + +variable "VITE_GOOGLE_CLIENT_ID" { + default = "" +} + +variable "VITE_GITHUB_CLIENT_ID" { + default = "" +} + +group "default" { + targets = ["app"] +} + +group "ci" { + targets = ["app-ci"] +} + +target "app" { + dockerfile = "Dockerfile" + context = "." + platforms = [ + "linux/amd64", + "linux/arm64" + ] + + tags = [ + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:${TAG}", + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:latest" + ] + + args = { + VITE_COUCHDB_URL = "${VITE_COUCHDB_URL}" + VITE_COUCHDB_USER = "${VITE_COUCHDB_USER}" + VITE_COUCHDB_PASSWORD = "${VITE_COUCHDB_PASSWORD}" + APP_BASE_URL = "${APP_BASE_URL}" + VITE_GOOGLE_CLIENT_ID = "${VITE_GOOGLE_CLIENT_ID}" + VITE_GITHUB_CLIENT_ID = "${VITE_GITHUB_CLIENT_ID}" + NODE_ENV = "production" + } + + # Gitea registry caching + cache-from = [ + "type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache" + ] + + cache-to = [ + "type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache,mode=max" + ] +} + +# CI-specific target with commit SHA tagging +target "app-ci" { + inherits = ["app"] + tags = [ + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:${GITEA_SHA}", + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:latest" + ] + + # Enhanced CI-specific features + attest = [ + "type=provenance,mode=max", + "type=sbom" + ] + + # CI registry push + output = ["type=registry"] +} + +# Development target for local builds +target "dev" { + inherits = ["app"] + platforms = ["linux/amd64"] + tags = ["rxminder:dev"] + + # Local caching only + cache-from = ["type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache"] + cache-to = ["type=registry,ref=${GITEA_REGISTRY}/${GITEA_REPOSITORY}:buildcache"] + + # Load locally instead of push + output = ["type=docker"] +} + +# Production target with full attestations +target "prod" { + inherits = ["app-ci"] + + # Production-specific tags + tags = [ + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:prod-${TAG}", + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:production" + ] + + # Full security attestations for production + attest = [ + "type=provenance,mode=max", + "type=sbom" + ] +} + +# Staging target +target "staging" { + inherits = ["app"] + platforms = ["linux/amd64"] # Single platform for staging + + tags = [ + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:staging-${TAG}", + "${GITEA_REGISTRY}/${GITEA_REPOSITORY}:staging" + ] + + # Staging-specific build args + args = { + VITE_COUCHDB_URL = "${VITE_COUCHDB_URL}" + VITE_COUCHDB_USER = "${VITE_COUCHDB_USER}" + VITE_COUCHDB_PASSWORD = "${VITE_COUCHDB_PASSWORD}" + APP_BASE_URL = "http://staging.localhost:8080" + VITE_GOOGLE_CLIENT_ID = "${VITE_GOOGLE_CLIENT_ID}" + VITE_GITHUB_CLIENT_ID = "${VITE_GITHUB_CLIENT_ID}" + NODE_ENV = "staging" + } + + output = ["type=registry"] +} diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml new file mode 100644 index 0000000..6428001 --- /dev/null +++ b/.gitea/workflows/ci-cd.yml @@ -0,0 +1,178 @@ +name: Build and Deploy + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +env: + # Use environment variables for registry configuration + REGISTRY: ${{ vars.GITEA_REGISTRY || secrets.GITEA_REGISTRY || 'ghcr.io' }} + IMAGE_NAME: ${{ gitea.repository }} + +jobs: + build: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: gitea.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./docker + platforms: linux/amd64,linux/arm64 + push: ${{ gitea.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VITE_COUCHDB_URL=${{ vars.VITE_COUCHDB_URL || 'http://localhost:5984' }} + VITE_COUCHDB_USER=${{ vars.VITE_COUCHDB_USER || 'admin' }} + VITE_COUCHDB_PASSWORD=${{ secrets.VITE_COUCHDB_PASSWORD || 'change-this-secure-password' }} + APP_BASE_URL=${{ vars.APP_BASE_URL || 'http://localhost:8080' }} + VITE_GOOGLE_CLIENT_ID=${{ vars.VITE_GOOGLE_CLIENT_ID || '' }} + VITE_GITHUB_CLIENT_ID=${{ vars.VITE_GITHUB_CLIENT_ID || '' }} + NODE_ENV=production + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + + - name: Build with Bake (Alternative) + if: false # Set to true to use bake instead + uses: docker/bake-action@v4 + with: + workdir: ./docker + files: docker-bake.hcl + targets: prod + push: ${{ gitea.event_name != 'pull_request' }} + + test: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run linting + run: bun run lint + + - name: Run type checking + run: bun run type-check + + - name: Run tests + run: bun run test + + - name: Run integration tests + run: bun run test:integration + + security: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + if: gitea.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run security audit + run: | + # Install and run security audit tools + npm audit --audit-level moderate || true + + - name: Scan Docker image for vulnerabilities + uses: aquasecurity/trivy-action@master + with: + image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}' + format: 'table' + exit-code: '0' + + deploy: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + needs: [build, test] + if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push' + environment: production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy to production + run: | + echo "Deploying to production server..." + + # Example deployment script + # You would typically SSH to your server and update the containers + + # Install kubectl if deploying to Kubernetes + # curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + # chmod +x kubectl && sudo mv kubectl /usr/local/bin/ + + # Or deploy via docker-compose + # ssh user@server "cd /app && docker-compose pull && docker-compose up -d" + + echo "Deployment placeholder - configure your deployment method" + + - name: Notify deployment status + if: always() + run: | + if [ "${{ job.status }}" == "success" ]; then + echo "✅ Deployment successful" + # Send success notification (webhook, email, etc.) + else + echo "❌ Deployment failed" + # Send failure notification + fi + + cleanup: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + needs: [build, test, deploy] + if: always() && gitea.ref == 'refs/heads/main' + + steps: + - name: Cleanup old images + run: | + echo "Cleaning up old container images..." + # Add cleanup logic for old images in registry + # This helps manage storage costs + echo "Cleanup placeholder - implement registry cleanup" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4cbab5e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,90 @@ +# Bug Report + +## 🐛 Bug Description + +A clear and concise description of the bug. + +## 🔄 Steps to Reproduce + +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## ✅ Expected Behavior + +A clear and concise description of what you expected to happen. + +## ❌ Actual Behavior + +A clear and concise description of what actually happened. + +## 📱 Environment + +**Desktop:** + +- OS: [e.g. Windows 10, macOS 12.0, Ubuntu 20.04] +- Browser: [e.g. Chrome 96, Firefox 95, Safari 15] +- Version: [e.g. 22] + +**Mobile:** + +- Device: [e.g. iPhone 13, Samsung Galaxy S21] +- OS: [e.g. iOS 15.1, Android 12] +- Browser: [e.g. Safari, Chrome] + +**Application:** + +- Version: [e.g. 1.2.0] +- Environment: [e.g. Local Development, Production] +- Authentication Method: [e.g. Email/Password, Google OAuth] + +## 📸 Screenshots + +If applicable, add screenshots to help explain your problem. + +## 📝 Additional Context + +Add any other context about the problem here. + +### Error Messages + +``` +Paste any error messages here +``` + +### Console Logs + +``` +Paste relevant console logs here +``` + +### Network Requests + +If the issue involves API calls, include relevant network request/response information. + +## 🔧 Troubleshooting Attempted + +- [ ] Cleared browser cache +- [ ] Tried incognito/private browsing +- [ ] Checked browser console for errors +- [ ] Verified internet connection +- [ ] Tried different browser +- [ ] Logged out and back in + +## 🏥 Medical Context (if applicable) + +- [ ] This affects medication reminders +- [ ] This affects dose tracking +- [ ] This could impact patient safety +- [ ] This involves sensitive health data + +**Priority Level:** [Low / Medium / High / Critical] + +## 📋 Checklist + +- [ ] I have searched existing issues to ensure this is not a duplicate +- [ ] I have provided clear steps to reproduce +- [ ] I have included environment details +- [ ] I have added relevant screenshots/logs +- [ ] I have marked appropriate priority level diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6721094 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,194 @@ +name: 🐛 Bug Report +description: Report a bug or issue with the application +title: '[BUG] ' +labels: ['bug', 'needs-triage'] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report a bug! Please fill out the information below to help us reproduce and fix the issue quickly. + + - type: checkboxes + id: pre-check + attributes: + label: Pre-submission Checklist + description: Please verify these items before submitting + options: + - label: I have searched existing issues to ensure this is not a duplicate + required: true + - label: I have provided clear steps to reproduce the issue + required: true + - label: I have tested this in the latest version + required: true + + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is + placeholder: Describe the bug... + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Clear steps to reproduce the behavior + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What you expected to happen + placeholder: I expected... + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened + placeholder: Instead... + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: Severity + description: How severe is this issue? + options: + - Low - Minor inconvenience + - Medium - Noticeable issue that doesn't block usage + - High - Significant issue that impacts functionality + - Critical - Application is unusable or data loss + validations: + required: true + + - type: dropdown + id: environment + attributes: + label: Environment + description: Where did this occur? + options: + - Local Development + - Docker Environment + - Production + - Staging + validations: + required: true + + - type: input + id: os + attributes: + label: Operating System + description: Your operating system + placeholder: e.g., Windows 11, macOS 13.0, Ubuntu 22.04 + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser + description: Browser and version + placeholder: e.g., Chrome 119, Firefox 118, Safari 17 + validations: + required: true + + - type: input + id: version + attributes: + label: Application Version + description: Version of the application + placeholder: e.g., 1.2.0 + validations: + required: false + + - type: dropdown + id: auth-method + attributes: + label: Authentication Method + description: How were you authenticated when this occurred? + options: + - Not authenticated + - Email/Password + - Google OAuth + - GitHub OAuth + - Admin Account + validations: + required: false + + - type: textarea + id: error-messages + attributes: + label: Error Messages + description: Any error messages you received + placeholder: Paste error messages here... + render: text + validations: + required: false + + - type: textarea + id: console-logs + attributes: + label: Console Logs + description: Relevant browser console logs + placeholder: Paste console logs here... + render: text + validations: + required: false + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Add screenshots to help explain the problem + placeholder: Drag and drop screenshots here... + validations: + required: false + + - type: checkboxes + id: troubleshooting + attributes: + label: Troubleshooting Attempted + description: What troubleshooting steps have you tried? + options: + - label: Cleared browser cache + - label: Tried incognito/private browsing + - label: Checked browser console for errors + - label: Verified internet connection + - label: Tried different browser + - label: Logged out and back in + - label: Restarted the application + + - type: checkboxes + id: medical-impact + attributes: + label: Medical Context + description: Does this issue affect medication management? (Check all that apply) + options: + - label: Affects medication reminders + - label: Affects dose tracking + - label: Could impact patient safety + - label: Involves sensitive health data + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any other context about the problem + placeholder: Add any other context here... + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..7241bf1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,120 @@ +name: 📚 Documentation Issue +description: Report problems with documentation or suggest improvements +title: '[DOCS] ' +labels: ['documentation', 'needs-triage'] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Help us improve our documentation! Report issues or suggest enhancements to make it more helpful. + + - type: dropdown + id: doc-type + attributes: + label: Documentation Type + description: What type of documentation needs attention? + options: + - README.md + - API Documentation + - Security Guide + - Deployment Guide + - Contributing Guide + - Code Comments + - User Guide + - Setup Instructions + - Other + validations: + required: true + + - type: dropdown + id: issue-type + attributes: + label: Issue Type + description: What kind of documentation issue is this? + options: + - Missing information + - Incorrect information + - Unclear instructions + - Outdated content + - Broken links + - Formatting issues + - Spelling/grammar errors + - Enhancement suggestion + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: Describe the documentation issue or improvement + placeholder: The documentation issue is... + validations: + required: true + + - type: input + id: location + attributes: + label: Document Location + description: Which file or section needs attention? + placeholder: e.g., README.md line 45, docs/API.md section "Authentication" + validations: + required: true + + - type: textarea + id: current-content + attributes: + label: Current Content (if applicable) + description: Quote the current text that needs to be changed + placeholder: Current text... + validations: + required: false + + - type: textarea + id: suggested-content + attributes: + label: Suggested Content + description: Provide the corrected or improved content + placeholder: Suggested replacement... + validations: + required: false + + - type: dropdown + id: audience + attributes: + label: Target Audience + description: Who is the primary audience for this documentation? + options: + - End users + - Developers + - System administrators + - Healthcare providers + - Contributors + - All audiences + validations: + required: true + + - type: checkboxes + id: impact + attributes: + label: Impact + description: What areas does this documentation issue affect? + options: + - label: New user onboarding + - label: Development setup + - label: Deployment process + - label: Security configuration + - label: API usage + - label: Troubleshooting + - label: Contributing process + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any other relevant information + placeholder: Additional context... + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e3c0622 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,173 @@ +# Feature Request + +## 🚀 Feature Description + +A clear and concise description of the feature you'd like to see implemented. + +## 💡 Problem Statement + +What problem does this feature solve? What user need does it address? + +## 🎯 Proposed Solution + +Describe your proposed solution in detail. How should this feature work? + +## 🏥 Medical Use Case + +Explain how this feature would improve medication management or patient outcomes. + +## 📱 User Experience + +Describe the user journey and interface for this feature. + +### **User Interface Mockups** + +If applicable, add mockups, wireframes, or descriptions of the UI. + +### **User Flow** + +1. User navigates to... +2. User clicks/taps... +3. System displays... +4. User completes... + +## 🔧 Technical Considerations + +### **Implementation Complexity** + +- [ ] Simple (few hours) +- [ ] Medium (few days) +- [ ] Complex (few weeks) +- [ ] Major (significant architecture changes) + +### **Affected Components** + +- [ ] Frontend UI +- [ ] Authentication system +- [ ] Database schema +- [ ] Email notifications +- [ ] Mobile responsiveness +- [ ] API endpoints +- [ ] Third-party integrations + +### **Dependencies** + +List any external libraries, services, or system changes needed. + +## 🎨 Design Requirements + +### **Visual Design** + +- [ ] Follows existing design system +- [ ] Requires new design patterns +- [ ] Needs accessibility considerations +- [ ] Mobile-first approach needed + +### **Responsive Behavior** + +Describe how this feature should work on different screen sizes. + +## 📊 Success Metrics + +How will we measure the success of this feature? + +- [ ] User engagement metrics +- [ ] Medication adherence improvement +- [ ] User satisfaction scores +- [ ] Performance metrics +- [ ] Error rate reduction + +## 🔒 Security & Privacy + +### **Data Handling** + +- [ ] Handles sensitive health data +- [ ] Requires data encryption +- [ ] Needs audit logging +- [ ] Affects user privacy + +### **Compliance** + +- [ ] GDPR considerations +- [ ] HIPAA considerations (if applicable) +- [ ] Data retention policies +- [ ] User consent requirements + +## 🌍 Accessibility + +- [ ] Screen reader compatible +- [ ] Keyboard navigation support +- [ ] Color contrast compliant +- [ ] Mobile accessibility +- [ ] Language localization needed + +## 🔄 Alternative Solutions + +What other approaches have you considered? Why is this the preferred solution? + +## 📚 Additional Context + +### **Similar Features** + +Are there similar features in other applications that work well? + +### **User Research** + +Any user feedback, surveys, or research supporting this feature? + +### **Priority Justification** + +Why should this feature be prioritized? + +## 🎯 Acceptance Criteria + +Define specific, testable criteria for when this feature is complete: + +- [ ] Criterion 1 +- [ ] Criterion 2 +- [ ] Criterion 3 + +## 📋 Implementation Plan (Optional) + +If you have ideas for implementation: + +### **Phase 1: Foundation** + +- [ ] Database changes +- [ ] API endpoints +- [ ] Basic UI components + +### **Phase 2: Core Feature** + +- [ ] Main functionality +- [ ] User interface +- [ ] Basic testing + +### **Phase 3: Polish** + +- [ ] Advanced features +- [ ] Performance optimization +- [ ] Comprehensive testing + +## 🏷️ Labels + +Please suggest appropriate labels: + +- [ ] enhancement +- [ ] ui/ux +- [ ] backend +- [ ] frontend +- [ ] security +- [ ] accessibility +- [ ] documentation +- [ ] high-priority +- [ ] good-first-issue + +## 📋 Checklist + +- [ ] I have searched existing issues to ensure this is not a duplicate +- [ ] I have clearly described the problem and solution +- [ ] I have considered the user experience impact +- [ ] I have thought about technical implementation +- [ ] I have considered security and privacy implications +- [ ] I have defined success criteria diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..7e1fc26 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,218 @@ +name: ✨ Feature Request +description: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: ['enhancement', 'needs-triage'] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thank you for suggesting a new feature! Please provide detailed information to help us understand and evaluate your request. + + - type: checkboxes + id: pre-check + attributes: + label: Pre-submission Checklist + description: Please verify these items before submitting + options: + - label: I have searched existing issues to ensure this is not a duplicate + required: true + - label: I have clearly described the problem and solution + required: true + - label: I have considered the user experience impact + required: true + + - type: textarea + id: feature-description + attributes: + label: Feature Description + description: A clear and concise description of the feature you'd like to see + placeholder: Describe the feature... + validations: + required: true + + - type: textarea + id: problem-statement + attributes: + label: Problem Statement + description: What problem does this feature solve? What user need does it address? + placeholder: This feature would solve... + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: Describe your proposed solution in detail + placeholder: The feature should work by... + validations: + required: true + + - type: textarea + id: medical-use-case + attributes: + label: Medical Use Case + description: How would this improve medication management or patient outcomes? + placeholder: This would help patients by... + validations: + required: true + + - type: dropdown + id: user-type + attributes: + label: Primary User Type + description: Who would primarily benefit from this feature? + options: + - Patients managing their own medications + - Caregivers managing medications for others + - Healthcare providers + - System administrators + - All users + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: Priority Level + description: How important is this feature? + options: + - Low - Nice to have enhancement + - Medium - Would improve user experience + - High - Important for better outcomes + - Critical - Essential for core functionality + validations: + required: true + + - type: dropdown + id: complexity + attributes: + label: Implementation Complexity + description: How complex do you think this feature would be to implement? + options: + - Simple (few hours) + - Medium (few days) + - Complex (few weeks) + - Major (significant architecture changes) + - Unknown + validations: + required: false + + - type: checkboxes + id: affected-components + attributes: + label: Affected Components + description: Which parts of the system would this feature affect? + options: + - label: Frontend UI + - label: Authentication system + - label: Database schema + - label: Email notifications + - label: Mobile responsiveness + - label: API endpoints + - label: Third-party integrations + - label: Admin interface + + - type: textarea + id: user-flow + attributes: + label: User Experience Flow + description: Describe the user journey for this feature + placeholder: | + 1. User navigates to... + 2. User clicks/taps... + 3. System displays... + 4. User completes... + validations: + required: true + + - type: textarea + id: success-metrics + attributes: + label: Success Metrics + description: How will we measure the success of this feature? + placeholder: | + - User engagement metrics + - Medication adherence improvement + - User satisfaction scores + validations: + required: false + + - type: checkboxes + id: design-requirements + attributes: + label: Design Requirements + description: What design considerations are important? + options: + - label: Follows existing design system + - label: Requires new design patterns + - label: Needs accessibility considerations + - label: Mobile-first approach needed + - label: Dark/light theme support + + - type: checkboxes + id: security-privacy + attributes: + label: Security & Privacy Considerations + description: Does this feature involve sensitive data or security concerns? + options: + - label: Handles sensitive health data + - label: Requires data encryption + - label: Needs audit logging + - label: Affects user privacy + - label: Requires user consent + - label: GDPR compliance needed + - label: HIPAA considerations + + - type: checkboxes + id: accessibility + attributes: + label: Accessibility Requirements + description: What accessibility features are needed? + options: + - label: Screen reader compatible + - label: Keyboard navigation support + - label: Color contrast compliant + - label: Mobile accessibility + - label: Language localization needed + + - type: textarea + id: alternatives + attributes: + label: Alternative Solutions + description: What other approaches have you considered? + placeholder: I also considered... + validations: + required: false + + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + description: Define specific, testable criteria for when this feature is complete + placeholder: | + - [ ] Criterion 1 + - [ ] Criterion 2 + - [ ] Criterion 3 + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any other context, mockups, or references + placeholder: Additional information... + validations: + required: false + + - type: textarea + id: similar-features + attributes: + label: Similar Features + description: Are there similar features in other applications that work well? + placeholder: Similar implementations I've seen... + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..00d29a6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,179 @@ +# Pull Request + +## 📝 Description + +Brief description of changes made in this pull request. + +## 🔗 Related Issues + +Fixes #(issue_number) +Closes #(issue_number) +Related to #(issue_number) + +## 🎯 Type of Change + +- [ ] 🐛 Bug fix (non-breaking change which fixes an issue) +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] 📚 Documentation update +- [ ] 🔧 Code refactoring (no functional changes) +- [ ] ⚡ Performance improvement +- [ ] 🧪 Test coverage improvement +- [ ] 🔒 Security enhancement +- [ ] 🎨 UI/UX improvement + +## 🧪 Testing + +Describe the tests you ran to verify your changes. + +### **Test Environment** + +- [ ] Local development +- [ ] Docker environment +- [ ] Production-like environment + +### **Test Cases** + +- [ ] Unit tests pass +- [ ] Integration tests pass +- [ ] Manual testing completed +- [ ] Cross-browser testing (if UI changes) +- [ ] Mobile testing (if responsive changes) +- [ ] Accessibility testing (if UI changes) + +### **New Tests Added** + +- [ ] Unit tests for new functionality +- [ ] Integration tests for API changes +- [ ] End-to-end tests for user flows + +## 📱 Screenshots (if applicable) + +Include screenshots for UI changes. + +### **Before** + +[Add screenshot of current state] + +### **After** + +[Add screenshot of new state] + +### **Mobile View** + +[Add mobile screenshots if applicable] + +## 🔒 Security Considerations + +- [ ] No sensitive data exposed in logs +- [ ] Authentication/authorization properly implemented +- [ ] Input validation in place +- [ ] SQL injection prevention (if applicable) +- [ ] XSS prevention (if applicable) +- [ ] CSRF protection maintained + +## 📊 Performance Impact + +- [ ] No performance degradation +- [ ] Performance improvements measured +- [ ] Database queries optimized +- [ ] Bundle size impact acceptable +- [ ] Memory usage acceptable + +## 🔄 Breaking Changes + +If this is a breaking change, describe: + +1. What breaks +2. Migration path for users +3. Version bump requirements + +## 📚 Documentation + +- [ ] Code is self-documenting with clear naming +- [ ] Complex logic has comments +- [ ] API documentation updated (if applicable) +- [ ] README updated (if applicable) +- [ ] User documentation updated (if applicable) + +## 📋 Checklist + +### **Code Quality** + +- [ ] Code follows project style guidelines +- [ ] Self-review of code completed +- [ ] Code is commented where necessary +- [ ] No console.log statements left in code +- [ ] Error handling implemented properly + +### **Testing & Validation** + +- [ ] All tests pass locally +- [ ] TypeScript compilation succeeds +- [ ] ESLint passes without errors +- [ ] Build succeeds without warnings +- [ ] Manual testing completed + +### **Review Preparation** + +- [ ] Pull request title is descriptive +- [ ] Pull request description is complete +- [ ] Commits are atomic and well-described +- [ ] No merge conflicts +- [ ] Base branch is correct + +### **Deployment Readiness** + +- [ ] Environment variables documented (if new) +- [ ] Database migrations included (if needed) +- [ ] Docker configuration updated (if needed) +- [ ] Deployment scripts updated (if needed) + +## 🎯 Review Focus Areas + +Please pay special attention to: + +- [ ] Security implications +- [ ] Performance impact +- [ ] Error handling +- [ ] User experience +- [ ] Code maintainability +- [ ] Test coverage + +## 📝 Additional Notes + +Any additional information for reviewers: + +### **Design Decisions** + +Explain any significant design or architecture decisions made. + +### **Trade-offs** + +Describe any trade-offs made and why they were necessary. + +### **Future Work** + +List any follow-up work that should be done in future PRs. + +## 🚀 Deployment Notes + +Special considerations for deployment: + +- [ ] Requires environment variable changes +- [ ] Requires database migration +- [ ] Requires cache clearing +- [ ] Requires dependency updates +- [ ] No special deployment requirements + +--- + +**Reviewer Checklist:** + +- [ ] Code review completed +- [ ] Tests reviewed and verified +- [ ] Documentation reviewed +- [ ] Security review completed (if applicable) +- [ ] Performance review completed (if applicable) +- [ ] Breaking changes noted and approved +- [ ] Deployment considerations reviewed diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml new file mode 100644 index 0000000..a982367 --- /dev/null +++ b/.github/workflows/build-deploy.yml @@ -0,0 +1,110 @@ +name: Build and Deploy + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./docker + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VITE_COUCHDB_URL=${{ vars.VITE_COUCHDB_URL || 'http://localhost:5984' }} + VITE_COUCHDB_USER=${{ vars.VITE_COUCHDB_USER || 'admin' }} + VITE_COUCHDB_PASSWORD=${{ secrets.VITE_COUCHDB_PASSWORD || 'change-this-secure-password' }} + APP_BASE_URL=${{ vars.APP_BASE_URL || 'http://localhost:8080' }} + VITE_GOOGLE_CLIENT_ID=${{ vars.VITE_GOOGLE_CLIENT_ID || '' }} + VITE_GITHUB_CLIENT_ID=${{ vars.VITE_GITHUB_CLIENT_ID || '' }} + NODE_ENV=production + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build with Bake (Alternative) + if: false # Set to true to use bake instead + uses: docker/bake-action@v4 + with: + workdir: ./docker + files: docker-bake.hcl + targets: prod + push: ${{ github.event_name != 'pull_request' }} + + test: + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run linting + run: bun run lint + + - name: Run type checking + run: bun run type-check + + - name: Run tests + run: bun run test + + deploy: + runs-on: ubuntu-latest + needs: [build, test] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + environment: production + + steps: + - name: Deploy to production + run: | + echo "Deploy to production server" + # Add your deployment commands here + # Example: SSH to server and pull the new image diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09a71ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment files (contain sensitive data) +.env +.env.local +# .env.production - committed with placeholder values for deployment +.env.staging + +# Database data +couchdb-data/ + +# Playwright artifacts +test-results/ +playwright-report/ +playwright/.cache/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..cb57d28 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,8 @@ +# Optional: Add commit message linting here if needed +# For now, just ensure commit message is not empty +if [ -z "$(cat $1 | head -1)" ]; then + echo "Error: Commit message cannot be empty" + exit 1 +fi + +echo "✅ Commit message looks good!" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..56076fd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +# Run lint-staged for file-specific checks +bunx lint-staged + +echo "✅ Pre-commit checks passed!" diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..6ba4711 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,18 @@ +{ + "default": true, + "MD001": true, + "MD003": { + "style": "atx" + }, + "MD007": { + "indent": 2 + }, + "MD013": { + "line_length": 120 + }, + "MD024": { + "allow_different_nesting": true + }, + "MD033": false, + "MD041": false +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c7dbd0b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,45 @@ +# Prettier ignore file +# See https://prettier.io/docs/en/ignore.html + +# Dependencies +node_modules/ +dist/ +build/ + +# Generated files +*.min.js +*.min.css + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml +bun.lockb + +# Logs +*.log + +# Coverage directory used by tools like istanbul +coverage/ + +# Docker +Dockerfile* +.dockerignore + +# Git +.git/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# CouchDB data +couchdb-data/ + +# Test outputs +test-results/ +playwright-report/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f1c4a33 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,41 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 120, + "proseWrap": "preserve" + } + }, + { + "files": "*.json", + "options": { + "printWidth": 120 + } + }, + { + "files": "*.yml", + "options": { + "tabWidth": 2 + } + }, + { + "files": "*.yaml", + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/.secretlintrc.json b/.secretlintrc.json new file mode 100644 index 0000000..5135f98 --- /dev/null +++ b/.secretlintrc.json @@ -0,0 +1,12 @@ +{ + "rules": [ + { + "id": "@secretlint/secretlint-rule-preset-recommend" + } + ], + "allowMessageIds": [], + "disabledMessages": [], + "reporterOptions": { + "formatter": "table" + } +} diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..23862df --- /dev/null +++ b/App.tsx @@ -0,0 +1,939 @@ +import React, { + useState, + useEffect, + useMemo, + useCallback, + useRef, +} from 'react'; +import { generateSchedule, generateReminderSchedule } from './utils/schedule'; +import { + Medication, + Dose, + DoseStatus, + HistoricalDose, + User, + UserSettings, + TakenDoses, + CustomReminder, + ScheduleItem, + DailyStat, + MedicationStat, + UserRole, +} from './types'; + +// Component imports - organized by feature +import { + AddMedicationModal, + EditMedicationModal, + ManageMedicationsModal, + DoseCard, +} from './components/medication'; +import { + AuthPage, + AvatarDropdown, + ChangePasswordModal, +} from './components/auth'; +import { AdminInterface } from './components/admin'; +import { + AccountModal, + AddReminderModal, + EditReminderModal, + HistoryModal, + ManageRemindersModal, + OnboardingModal, + StatsModal, +} from './components/modals'; +import { BarChart, ReminderCard, ThemeSwitcher } from './components/ui'; + +// Icon and utility imports +import { + PillIcon, + PlusIcon, + MenuIcon, + HistoryIcon, + SunIcon, + SunsetIcon, + MoonIcon, + SearchIcon, + SettingsIcon, + BellIcon, + BarChartIcon, +} from './components/icons/Icons'; +import { useUser } from './contexts/UserContext'; +import { dbService } from './services/couchdb.factory'; +import { databaseSeeder } from './services/database.seeder'; + +const Header: React.FC<{ + onAdd: () => void; + onManage: () => void; + onManageReminders: () => void; + onHistory: () => void; + onStats: () => void; + onAccount: () => void; + onAdmin: () => void; + onChangePassword: () => void; + user: User; + onLogout: () => void; +}> = ({ + onAdd, + onManage, + onManageReminders, + onHistory, + onStats, + onAccount, + onAdmin, + onChangePassword, + user, + onLogout, +}) => ( +
+
+
+
+ +
+

+ Medication Reminder +

+
+
+ + + + + +
+ + +
+ +
+
+
+); + +const EmptyState: React.FC<{ onAdd: () => void }> = ({ onAdd }) => ( +
+
+ +
+

+ No Medications Scheduled +

+

+ Get started by adding your first medication. +

+
+ +
+
+); + +const groupDetails: { + [key: string]: { + icon: React.FC>; + iconClass: string; + }; +} = { + Morning: { icon: SunIcon, iconClass: 'text-amber-500' }, + Afternoon: { icon: SunIcon, iconClass: 'text-sky-500' }, + Evening: { + icon: SunsetIcon, + iconClass: 'text-indigo-500 dark:text-indigo-400', + }, + Night: { icon: MoonIcon, iconClass: 'text-slate-500 dark:text-slate-400' }, +}; + +const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => { + const { logout, updateUser } = useUser(); + + const [medications, setMedications] = useState([]); + const [customReminders, setCustomReminders] = useState([]); + const [takenDosesDoc, setTakenDosesDoc] = useState(null); + const [settings, setSettings] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const [currentTime, setCurrentTime] = useState(() => new Date()); + const [isAddModalOpen, setAddModalOpen] = useState(false); + const [isManageModalOpen, setManageModalOpen] = useState(false); + const [isHistoryModalOpen, setHistoryModalOpen] = useState(false); + const [isAccountModalOpen, setAccountModalOpen] = useState(false); + const [isStatsModalOpen, setStatsModalOpen] = useState(false); + const [editingMedication, setEditingMedication] = useState( + null + ); + const [searchQuery, setSearchQuery] = useState(''); + const [isOnboardingOpen, setOnboardingOpen] = useState(false); + const [isAdminInterfaceOpen, setAdminInterfaceOpen] = useState(false); + const [isChangePasswordOpen, setChangePasswordOpen] = useState(false); + + const [isManageRemindersOpen, setManageRemindersOpen] = useState(false); + const [isAddReminderOpen, setAddReminderOpen] = useState(false); + const [editingReminder, setEditingReminder] = useState( + null + ); + const [snoozedDoses, setSnoozedDoses] = useState>({}); + + const notificationTimers = useRef>({}); + + const takenDoses = useMemo(() => takenDosesDoc?.doses ?? {}, [takenDosesDoc]); + + useEffect(() => { + // Don't try to fetch data if user._id is not available + if (!user._id) { + console.warn('Skipping data fetch: user._id is not available'); + return; + } + + const fetchData = async () => { + try { + setIsLoading(true); + setError(null); + + console.log('Fetching data for user:', user._id); + + const [medsData, remindersData, takenDosesData, settingsData] = + await Promise.all([ + dbService.getMedications(user._id), + dbService.getCustomReminders(user._id), + dbService.getTakenDoses(user._id), + dbService.getSettings(user._id), + ]); + + console.log('Data fetched successfully:', { + medications: medsData.length, + reminders: remindersData.length, + hasTakenDoses: !!takenDosesData, + hasSettings: !!settingsData, + }); + + setMedications(medsData); + setCustomReminders(remindersData); + setTakenDosesDoc(takenDosesData); + setSettings(settingsData); + + if (!settingsData.hasCompletedOnboarding) { + setOnboardingOpen(true); + } + } catch (e) { + setError('Failed to load your data. Please try again.'); + console.error('Error loading user data:', e); + console.error('User object:', user); + } finally { + setIsLoading(false); + } + }; + + // Add a small delay to ensure user state is fully settled + const timeoutId = setTimeout(() => { + fetchData(); + }, 100); + + return () => clearTimeout(timeoutId); + }, [user._id]); + + useEffect(() => { + if ( + settings?.notificationsEnabled && + 'Notification' in window && + Notification.permission === 'default' + ) { + Notification.requestPermission(); + } + }, [settings?.notificationsEnabled]); + + useEffect(() => { + const timer = setInterval(() => setCurrentTime(new Date()), 60000); + return () => clearInterval(timer); + }, []); + + const unifiedSchedule = useMemo(() => { + const medSchedule = generateSchedule(medications, currentTime); + const reminderSchedule = generateReminderSchedule( + customReminders, + currentTime + ); + const combined = [...medSchedule, ...reminderSchedule] as ScheduleItem[]; + return combined.sort( + (a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime() + ); + }, [medications, customReminders, currentTime]); + + const handleAddMedication = async (med: Omit) => { + const newMed = await dbService.addMedication(user._id, med); + setMedications(prev => [...prev, newMed]); + setAddModalOpen(false); + }; + + const handleDeleteMedication = async (medToDelete: Medication) => { + await dbService.deleteMedication(user._id, medToDelete); + setMedications(meds => meds.filter(med => med._id !== medToDelete._id)); + }; + + const handleUpdateMedication = async (updatedMed: Medication) => { + const savedMed = await dbService.updateMedication(user._id, updatedMed); + setMedications(meds => + meds.map(m => (m._id === savedMed._id ? savedMed : m)) + ); + setEditingMedication(null); + }; + + const handleAddReminder = async ( + reminder: Omit + ) => { + const newReminder = await dbService.addCustomReminder(user._id, reminder); + setCustomReminders(prev => [...prev, newReminder]); + setAddReminderOpen(false); + }; + + const handleUpdateReminder = async (updatedReminder: CustomReminder) => { + const savedReminder = await dbService.updateCustomReminder( + user._id, + updatedReminder + ); + setCustomReminders(reminders => + reminders.map(r => (r._id === savedReminder._id ? savedReminder : r)) + ); + setEditingReminder(null); + }; + + const handleDeleteReminder = async (reminderToDelete: CustomReminder) => { + await dbService.deleteCustomReminder(user._id, reminderToDelete); + setCustomReminders(reminders => + reminders.filter(r => r._id !== reminderToDelete._id) + ); + }; + + const handleOpenEditModal = (med: Medication) => { + setEditingMedication(med); + setManageModalOpen(false); + }; + + const handleOpenEditReminderModal = (reminder: CustomReminder) => { + setEditingReminder(reminder); + setManageRemindersOpen(false); + }; + + const handleToggleDose = useCallback( + async (doseId: string) => { + if (!takenDosesDoc) return; + const newDoses = { ...takenDosesDoc.doses }; + if (newDoses[doseId]) { + delete newDoses[doseId]; + } else { + newDoses[doseId] = new Date().toISOString(); + } + const updatedDoc = await dbService.updateTakenDoses(user._id, { + ...takenDosesDoc, + doses: newDoses, + }); + setTakenDosesDoc(updatedDoc); + }, + [takenDosesDoc, user._id] + ); + + const handleSnoozeDose = useCallback((doseId: string) => { + const SNOOZE_DURATION = 5 * 60 * 1000; // 5 minutes + const snoozedUntil = new Date(Date.now() + SNOOZE_DURATION).toISOString(); + setSnoozedDoses(prev => ({ ...prev, [doseId]: snoozedUntil })); + + // Clear existing timer and set a new one + if (notificationTimers.current[doseId]) { + clearTimeout(notificationTimers.current[doseId]); + delete notificationTimers.current[doseId]; + } + }, []); + + const getDoseStatus = useCallback( + (dose: Dose, doseTime: Date, now: Date): DoseStatus => { + if (takenDoses[dose.id]) return DoseStatus.TAKEN; + if (snoozedDoses[dose.id] && new Date(snoozedDoses[dose.id]) > now) + return DoseStatus.SNOOZED; + if (doseTime.getTime() < now.getTime()) return DoseStatus.MISSED; + return DoseStatus.UPCOMING; + }, + [takenDoses, snoozedDoses] + ); + + const scheduleWithStatus = useMemo(() => { + return unifiedSchedule + .map(item => { + if ('medicationId' in item) { + // It's a Dose + const medication = medications.find(m => m._id === item.medicationId); + if (!medication) return null; + + return { + ...item, + type: 'dose' as const, + medication, + status: getDoseStatus(item, item.scheduledTime, currentTime), + takenAt: takenDoses[item.id], + snoozedUntil: snoozedDoses[item.id] + ? new Date(snoozedDoses[item.id]) + : undefined, + }; + } else { + // It's a Custom Reminder + return { + ...item, + type: 'reminder' as const, + }; + } + }) + .filter((d): d is NonNullable => d !== null); + }, [ + unifiedSchedule, + medications, + getDoseStatus, + currentTime, + takenDoses, + snoozedDoses, + ]); + + useEffect(() => { + if ( + !settings?.notificationsEnabled || + !('Notification' in window) || + Notification.permission !== 'granted' + ) { + return; + } + + const now = new Date(); + const activeTimers = notificationTimers.current; + + scheduleWithStatus.forEach(item => { + const itemId = item.id; + if (activeTimers[itemId]) return; // Timer already set + + let timeToNotification = -1; + let notificationBody = ''; + let notificationTitle = ''; + + if (item.type === 'dose' && item.status === DoseStatus.UPCOMING) { + timeToNotification = item.scheduledTime.getTime() - now.getTime(); + notificationTitle = 'Time for your medication!'; + notificationBody = `${item.medication.name} (${item.medication.dosage})`; + } else if ( + item.type === 'dose' && + item.status === DoseStatus.SNOOZED && + item.snoozedUntil + ) { + timeToNotification = item.snoozedUntil.getTime() - now.getTime(); + notificationTitle = 'Snoozed Medication Reminder'; + notificationBody = `${item.medication.name} (${item.medication.dosage})`; + } else if (item.type === 'reminder' && item.scheduledTime > now) { + timeToNotification = item.scheduledTime.getTime() - now.getTime(); + notificationTitle = 'Reminder'; + notificationBody = item.title; + } + + if (timeToNotification > 0) { + activeTimers[itemId] = setTimeout(() => { + new Notification(notificationTitle, { + body: notificationBody, + tag: itemId, + }); + if (item.type === 'dose' && item.status === DoseStatus.SNOOZED) { + setSnoozedDoses(prev => { + const newSnoozed = { ...prev }; + delete newSnoozed[itemId]; + return newSnoozed; + }); + } + delete activeTimers[itemId]; + }, timeToNotification) as unknown as number; + } + }); + + return () => Object.values(activeTimers).forEach(clearTimeout); + }, [scheduleWithStatus, settings?.notificationsEnabled]); + + const filteredSchedule = useMemo( + () => + scheduleWithStatus.filter(item => { + if (item.type === 'reminder') { + return item.title.toLowerCase().includes(searchQuery.toLowerCase()); + } + return item.medication.name + .toLowerCase() + .includes(searchQuery.toLowerCase()); + }), + [scheduleWithStatus, searchQuery] + ); + + const groupedSchedule = useMemo(() => { + const groups: { [key: string]: typeof filteredSchedule } = { + Morning: [], + Afternoon: [], + Evening: [], + Night: [], + }; + filteredSchedule.forEach(item => { + const hour = item.scheduledTime.getHours(); + if (hour >= 5 && hour < 12) groups['Morning'].push(item); + else if (hour >= 12 && hour < 17) groups['Afternoon'].push(item); + else if (hour >= 17 && hour < 21) groups['Evening'].push(item); + else groups['Night'].push(item); + }); + return groups; + }, [filteredSchedule]); + + const medicationHistory = useMemo(() => { + const history: { date: string; doses: HistoricalDose[] }[] = []; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const now = new Date(); + for (let i = 0; i < 7; i++) { + const date = new Date(today); + date.setDate(date.getDate() - i); + const daySchedule = generateSchedule(medications, date); + if (daySchedule.length === 0 || date.getTime() > now.getTime()) continue; + const dosesForDay: HistoricalDose[] = daySchedule + .map(dose => { + const medication = medications.find(m => m._id === dose.medicationId); + return medication + ? { + id: dose.id, + medication, + scheduledTime: dose.scheduledTime, + status: getDoseStatus(dose, dose.scheduledTime, now), + takenAt: takenDoses[dose.id], + } + : null; + }) + .filter((d): d is NonNullable => d !== null); + if (dosesForDay.length > 0) + history.push({ + date: date.toISOString().split('T')[0], + doses: dosesForDay, + }); + } + return history; + }, [medications, takenDoses, getDoseStatus]); + + const { dailyStats, medicationStats } = useMemo(() => { + const today = new Date(); + today.setHours(23, 59, 59, 999); + const now = new Date(); + + const daily: DailyStat[] = []; + for (let i = 6; i >= 0; i--) { + const date = new Date(); + date.setHours(0, 0, 0, 0); + date.setDate(date.getDate() - i); + + const daySchedule = generateSchedule(medications, date); + const pastDoses = daySchedule.filter(d => d.scheduledTime < now); + + if (pastDoses.length === 0) { + daily.push({ date: date.toISOString().split('T')[0], adherence: 100 }); + continue; + } + + let takenCount = 0; + pastDoses.forEach(dose => { + if (takenDoses[dose.id]) { + takenCount++; + } + }); + + const adherence = (takenCount / pastDoses.length) * 100; + daily.push({ + date: date.toISOString().split('T')[0], + adherence: Math.round(adherence), + }); + } + + const statsByMedId: Record< + string, + { + taken: number; + missed: number; + upcoming: number; + medication: Medication; + lastTakenAt?: string; + } + > = {}; + medications.forEach(med => { + statsByMedId[med._id] = { + taken: 0, + missed: 0, + upcoming: 0, + medication: med, + }; + }); + + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + medications.forEach(med => { + for (let i = 0; i < 7; i++) { + const date = new Date(); + date.setDate(date.getDate() - i); + const daySchedule = generateSchedule([med], date); + + daySchedule.forEach(dose => { + const stat = statsByMedId[dose.medicationId]; + if (stat) { + const status = getDoseStatus(dose, dose.scheduledTime, now); + if (status === DoseStatus.TAKEN) { + stat.taken++; + const takenAt = takenDoses[dose.id]; + if ( + takenAt && + (!stat.lastTakenAt || + new Date(takenAt) > new Date(stat.lastTakenAt)) + ) { + stat.lastTakenAt = takenAt; + } + } else if (status === DoseStatus.MISSED) stat.missed++; + else if (status === DoseStatus.UPCOMING) stat.upcoming++; + } + }); + } + }); + + const medication: MedicationStat[] = Object.values(statsByMedId) + .map(stat => { + const totalPast = stat.taken + stat.missed; + const adherence = + totalPast > 0 ? Math.round((stat.taken / totalPast) * 100) : 100; + return { ...stat, adherence }; + }) + .sort((a, b) => a.medication.name.localeCompare(b.medication.name)); + + return { dailyStats: daily, medicationStats: medication }; + }, [medications, takenDoses, getDoseStatus]); + + const handleUpdateSettings = async (newSettings: UserSettings) => { + const updatedSettings = await dbService.updateSettings( + user._id, + newSettings + ); + setSettings(updatedSettings); + }; + + const handleDeleteAllData = async () => { + if ( + window.confirm( + 'Are you sure you want to delete all your medication data? This action cannot be undone.' + ) + ) { + await dbService.deleteAllUserData(user._id); + setMedications([]); + setCustomReminders([]); + const updatedTakenDoses = await dbService.getTakenDoses(user._id); + setTakenDosesDoc(updatedTakenDoses); + setAccountModalOpen(false); + } + }; + + const handleCompleteOnboarding = async () => { + if (settings) { + try { + const updatedSettings = await dbService.updateSettings(user._id, { + ...settings, + hasCompletedOnboarding: true, + }); + setSettings(updatedSettings); + setOnboardingOpen(false); + } catch (error) { + console.error('Failed to update onboarding status', error); + setOnboardingOpen(false); + } + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + return ( +
+
setAddModalOpen(true)} + onManage={() => setManageModalOpen(true)} + onManageReminders={() => setManageRemindersOpen(true)} + onHistory={() => setHistoryModalOpen(true)} + onStats={() => setStatsModalOpen(true)} + onAccount={() => setAccountModalOpen(true)} + onAdmin={() => setAdminInterfaceOpen(true)} + onChangePassword={() => setChangePasswordOpen(true)} + user={user} + onLogout={logout} + /> + +
+
+

+ Today's Schedule +

+ +
+ + {medications.length > 0 || customReminders.length > 0 ? ( + <> +
+
+
+ setSearchQuery(e.target.value)} + /> +
+ {filteredSchedule.length > 0 ? ( +
+ {Object.entries(groupedSchedule).map(([groupName, items]) => { + const scheduleItems = items as typeof filteredSchedule; + if (scheduleItems.length === 0) return null; + const Icon = groupDetails[groupName]?.icon; + return ( +
+
+ {Icon && ( +
+
    + {scheduleItems.map(item => + item.type === 'dose' ? ( + + ) : ( + + ) + )} +
+
+ ); + })} +
+ ) : ( +
+ +

+ No items found +

+

+ Your search for "{searchQuery}" did not match any items + scheduled for today. +

+
+ )} + + ) : ( + setAddModalOpen(true)} /> + )} +
+ + setAddModalOpen(false)} + onAdd={handleAddMedication} + /> + setManageModalOpen(false)} + medications={medications} + onDelete={handleDeleteMedication} + onEdit={handleOpenEditModal} + /> + setEditingMedication(null)} + medication={editingMedication} + onUpdate={handleUpdateMedication} + /> + setHistoryModalOpen(false)} + history={medicationHistory} + /> + setStatsModalOpen(false)} + dailyStats={dailyStats} + medicationStats={medicationStats} + /> + {settings && ( + setAccountModalOpen(false)} + user={user} + settings={settings} + onUpdateUser={updateUser} + onUpdateSettings={handleUpdateSettings} + onDeleteAllData={handleDeleteAllData} + /> + )} + + + setManageRemindersOpen(false)} + reminders={customReminders} + onAdd={() => { + setManageRemindersOpen(false); + setAddReminderOpen(true); + }} + onEdit={handleOpenEditReminderModal} + onDelete={handleDeleteReminder} + /> + setAddReminderOpen(false)} + onAdd={handleAddReminder} + /> + setEditingReminder(null)} + reminder={editingReminder} + onUpdate={handleUpdateReminder} + /> + + {/* Admin Interface - Only shown when opened */} + {isAdminInterfaceOpen && ( + setAdminInterfaceOpen(false)} /> + )} + + {/* Password Change Modal - Only shown when opened */} + {isChangePasswordOpen && ( + setChangePasswordOpen(false)} + onSuccess={() => { + alert('Password changed successfully!'); + setChangePasswordOpen(false); + }} + /> + )} +
+ ); +}; + +const App: React.FC = () => { + const { user, isLoading } = useUser(); + + // Run database seeding on app startup + useEffect(() => { + const runSeeding = async () => { + try { + console.log('🌱 Initializing database seeding...'); + await databaseSeeder.seedDatabase(); + } catch (error) { + console.error('❌ Database seeding failed:', error); + } + }; + + runSeeding(); + }, []); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!user) { + return ; + } + + return ; +}; + +export default App; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..71dbeff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,299 @@ +# Changelog + +All notable changes to the Medication Reminder App will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-09-05 + +### Added + +- **Complete Authentication System** + - Email/password authentication with bcrypt hashing + - OAuth integration (Google, GitHub) + - Email verification with Mailgun + - Password reset functionality + - Role-based access control (User, Admin) + +- **Medication Management** + - Add, edit, delete medications + - Flexible scheduling (Daily, Multiple times, Custom intervals) + - Visual medication cards with custom icons + - Medication history tracking + +- **Reminder System** + - Smart scheduling based on medication frequency + - Dose tracking (Taken, Missed, Upcoming) + - Custom reminders with personalized messages + - Adherence statistics and progress monitoring + +- **Admin Interface** + - Complete user management dashboard + - View all users with status and role information + - Suspend/activate user accounts + - Delete users with protection mechanisms + - Change user passwords + - Role assignment capabilities + +- **User Experience Features** + - Responsive design for mobile and desktop + - Dark/Light theme support + - Intuitive interface with modern design + - Onboarding flow for new users + - Avatar customization with image upload + - Settings management for preferences + +- **Analytics Dashboard** + - Daily adherence statistics with visual charts + - Medication-specific analytics + - Progress tracking over time + - Export capabilities for healthcare providers + +- **Infrastructure** + - Docker containerization with multi-stage builds + - CouchDB integration for data persistence + - Environment-based service factory pattern + - Production-ready nginx configuration + - Comprehensive health checks + +- **Security Features** + - Secure password hashing with bcrypt + - JWT-like token system for sessions + - Email verification for account activation + - Input validation and sanitization + - Role-based authorization + - Secure credential management + +- **Development Tools** + - TypeScript for type safety + - ESLint for code quality + - Automated setup and deployment scripts + - Comprehensive test suite + - Environment configuration management + +- **Documentation** + - Complete README with setup instructions + - API documentation with examples + - Security guide and best practices + - Deployment guide for various platforms + - Troubleshooting documentation + +### Technical Details + +- **Frontend**: React 19 with TypeScript, Vite build system +- **Backend**: CouchDB with localStorage fallback +- **Email**: Mailgun integration for verification and password reset +- **Deployment**: Docker Compose with nginx for production +- **Testing**: Jest integration tests for authentication flows +- **Package Management**: Bun for fast dependency management + +### Database Schema + +- Users collection with authentication and profile data +- Medications collection with scheduling information +- Settings collection for user preferences +- Taken doses collection for adherence tracking +- Reminders collection for custom user reminders + +### Security Implementations + +- Password requirements with strength validation +- Account status management (Pending, Active, Suspended) +- Email verification workflow +- Secure token generation for password reset +- Admin privilege separation +- Data privacy controls + +### Performance Features + +- Lazy loading for large datasets +- Optimized Docker images with multi-stage builds +- Static file serving with nginx +- Database indexing for efficient queries +- Responsive design for all screen sizes + +## [Unreleased] + +### Planned Features + +- Mobile app development (React Native) +- Push notifications for reminders +- Integration with health APIs (Apple Health, Google Fit) +- Medication interaction checking +- Prescription refill reminders +- Healthcare provider portal +- Advanced analytics with machine learning +- Multi-language support +- Backup and restore functionality +- API rate limiting improvements + +### Under Consideration + +- Voice commands for medication logging +- Barcode scanning for medication identification +- Family account management +- Telemedicine integration +- Insurance information management +- Side effect tracking +- Mood and symptom correlation +- Wearable device integration + +## Development Milestones + +### Phase 1: Core Functionality ✅ + +- [x] Basic medication tracking +- [x] User authentication +- [x] Reminder system +- [x] Data persistence + +### Phase 2: Enhanced Features ✅ + +- [x] Admin interface +- [x] Email integration +- [x] Analytics dashboard +- [x] Security hardening + +### Phase 3: Production Ready ✅ + +- [x] Docker deployment +- [x] Environment management +- [x] Documentation +- [x] Testing suite + +### Phase 4: Advanced Features (In Progress) + +- [ ] Mobile application +- [ ] Advanced analytics +- [ ] Healthcare integrations +- [ ] Multi-tenant support + +## Breaking Changes + +### Version 1.0.0 + +- Initial release - no breaking changes from previous versions +- Migration from localStorage-only to production CouchDB +- Environment variable restructuring for security + +## Migration Guide + +### From Development to Production + +1. Copy `.env.example` to `.env` +2. Configure CouchDB credentials +3. Set up Mailgun for email features +4. Run `./deploy.sh production` +5. Seed database with admin user + +### Database Migration + +- Automatic migration from localStorage to CouchDB +- Data import tools available for existing installations +- Backup and restore procedures documented + +## Security Updates + +### Version 1.0.0 + +- Implemented bcrypt password hashing +- Added JWT-like session management +- Configured secure email verification +- Established role-based access control +- Implemented input validation and sanitization + +## Performance Improvements + +### Version 1.0.0 + +- Optimized Docker build process +- Implemented lazy loading for large datasets +- Added database indexing for efficient queries +- Configured nginx for optimal static file serving +- Optimized React component rendering + +## Bug Fixes + +### Version 1.0.0 + +- Fixed authentication state management +- Resolved timezone handling in reminders +- Corrected medication scheduling edge cases +- Fixed mobile responsive design issues +- Resolved Docker environment variable handling + +## Contributors + +### Core Team + +- Lead Developer - Full-stack development and architecture +- UI/UX Designer - Interface design and user experience +- DevOps Engineer - Infrastructure and deployment +- Security Specialist - Security audit and hardening + +### Community Contributors + +- Documentation improvements +- Bug reports and testing +- Feature suggestions and feedback +- Translation contributions + +## Acknowledgments + +### Open Source Libraries + +- React team for the excellent frontend framework +- CouchDB team for the robust database system +- Mailgun for reliable email delivery services +- Docker team for containerization technology +- TypeScript team for enhanced development experience + +### Inspiration + +- Healthcare professionals providing feedback +- Patients sharing medication management challenges +- Open source community best practices +- Modern web development standards + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Release Notes + +### Version 1.0.0 - "Foundation Release" + +This inaugural release establishes the core foundation of the Medication Reminder App with enterprise-grade features and security. The application provides a complete medication management solution with professional-grade authentication, administration tools, and analytics capabilities. + +**Key Highlights:** + +- Production-ready Docker deployment +- Comprehensive user and admin interfaces +- Secure authentication with email verification +- Real-time medication tracking and analytics +- Mobile-responsive design +- Extensive documentation and security guides + +**Who Should Upgrade:** + +- All users moving from development to production +- Healthcare organizations requiring medication tracking +- Individuals seeking comprehensive medication management +- Developers needing a complete authentication reference + +**Migration Path:** + +- Follow the deployment guide for new installations +- Use the migration tools for existing data +- Review security guide for production deployment +- Test thoroughly in staging environment before production + +--- + +For technical support or questions about this release, please: + +- Check the documentation in the `docs/` directory +- Open an issue on GitHub for bug reports +- Contact the development team for enterprise support +- Join our community Discord for general questions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..53825eb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,524 @@ +# Contributing to Medication Reminder App + +Thank you for your interest in contributing to the Medication Reminder App! This document provides guidelines and information for contributors. + +## 🤝 How to Contribute + +### Reporting Issues + +Before creating an issue, please check if it already exists in our [issue tracker](https://github.com/your-username/rxminder/issues). + +#### Bug Reports + +- Use the bug report template +- Include steps to reproduce +- Provide system information +- Add screenshots if applicable + +#### Feature Requests + +- Use the feature request template +- Explain the problem you're trying to solve +- Describe your proposed solution +- Consider implementation complexity + +### Development Process + +#### 1. Fork and Clone + +```bash +# Fork the repository on GitHub +# Clone your fork +git clone https://github.com/your-username/rxminder.git +cd meds + +# Add upstream remote +git remote add upstream https://github.com/original-owner/meds.git +``` + +#### 2. Set Up Development Environment + +```bash +# Run setup script +./setup.sh + +# Or manual setup +bun install +cp .env.example .env +# Edit .env with your development values +docker compose -f docker/docker-compose.yaml up -d +``` + +#### 3. Create Feature Branch + +```bash +# Update main branch +git checkout main +git pull upstream main + +# Create feature branch +git checkout -b feature/your-feature-name +``` + +#### 4. Make Changes + +- Follow coding standards (see below) +- Write tests for new functionality +- Update documentation as needed +- Ensure all tests pass + +#### 5. Commit Changes + +```bash +# Stage changes +git add . + +# Commit with descriptive message +git commit -m "feat: add medication interaction checking + +- Implement drug interaction API integration +- Add warning UI components +- Include interaction severity levels +- Update medication form validation" +``` + +#### 6. Push and Create Pull Request + +```bash +# Push to your fork +git push origin feature/your-feature-name + +# Create pull request on GitHub +# Use the pull request template +# Link related issues +``` + +## 📝 Coding Standards + +### TypeScript/JavaScript + +```typescript +// Use TypeScript for all new code +// Define interfaces for data structures +interface Medication { + _id: string; + name: string; + dosage: string; + frequency: Frequency; +} + +// Use meaningful variable names +const medicationList = getMedications(); +const isUserAuthenticated = checkAuthStatus(); + +// Add JSDoc comments for complex functions +/** + * Calculates medication adherence percentage + * @param takenDoses - Number of doses taken + * @param totalDoses - Total number of scheduled doses + * @returns Adherence percentage (0-100) + */ +function calculateAdherence(takenDoses: number, totalDoses: number): number { + return totalDoses > 0 ? (takenDoses / totalDoses) * 100 : 0; +} +``` + +### React Components + +```tsx +// Use functional components with hooks +import React, { useState, useEffect } from 'react'; + +interface MedicationCardProps { + medication: Medication; + onEdit: (medication: Medication) => void; + onDelete: (id: string) => void; +} + +export const MedicationCard: React.FC = ({ medication, onEdit, onDelete }) => { + const [isLoading, setIsLoading] = useState(false); + + // Use descriptive event handlers + const handleEditClick = () => { + onEdit(medication); + }; + + const handleDeleteClick = async () => { + setIsLoading(true); + try { + await onDelete(medication._id); + } finally { + setIsLoading(false); + } + }; + + return
{/* Component JSX */}
; +}; +``` + +### CSS/Styling + +```css +/* Use BEM methodology for CSS classes */ +.medication-card { + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; +} + +.medication-card__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.medication-card__title { + font-size: 1.2rem; + font-weight: 600; + margin: 0; +} + +.medication-card--highlighted { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* Use CSS custom properties for theming */ +:root { + --primary-color: #007bff; + --secondary-color: #6c757d; + --success-color: #28a745; + --warning-color: #ffc107; + --danger-color: #dc3545; +} +``` + +### File Organization + +``` +src/ +├── components/ # Reusable UI components +│ ├── common/ # Generic components +│ ├── forms/ # Form-specific components +│ └── modals/ # Modal components +├── pages/ # Page-level components +├── hooks/ # Custom React hooks +├── services/ # API and business logic +├── utils/ # Utility functions +├── types/ # TypeScript type definitions +├── contexts/ # React context providers +└── assets/ # Static assets +``` + +## 🧪 Testing Guidelines + +### Unit Tests + +```typescript +// Test filename: ComponentName.test.tsx +import { render, screen, fireEvent } from '@testing-library/react'; +import { MedicationCard } from './MedicationCard'; + +describe('MedicationCard', () => { + const mockMedication = { + _id: '1', + name: 'Aspirin', + dosage: '100mg', + frequency: Frequency.Daily + }; + + const mockOnEdit = jest.fn(); + const mockOnDelete = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders medication information correctly', () => { + render( + + ); + + expect(screen.getByText('Aspirin')).toBeInTheDocument(); + expect(screen.getByText('100mg')).toBeInTheDocument(); + }); + + it('calls onEdit when edit button is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: /edit/i })); + expect(mockOnEdit).toHaveBeenCalledWith(mockMedication); + }); +}); +``` + +### Integration Tests + +```typescript +// Test authentication flow +describe('Authentication Integration', () => { + beforeEach(() => { + // Clear localStorage and reset mocks + localStorage.clear(); + jest.clearAllMocks(); + }); + + it('allows user to register and login', async () => { + // Test registration + const registrationResult = await authService.register('test@example.com', 'Password123!', 'Test User'); + + expect(registrationResult.user.email).toBe('test@example.com'); + expect(registrationResult.user.status).toBe(AccountStatus.PENDING); + + // Test email verification + await authService.verifyEmail(registrationResult.verificationToken.token); + + // Test login + const loginResult = await authService.login({ + email: 'test@example.com', + password: 'Password123!', + }); + + expect(loginResult.user.status).toBe(AccountStatus.ACTIVE); + expect(loginResult.accessToken).toBeDefined(); + }); +}); +``` + +### Running Tests + +```bash +# Run all tests +bun test + +# Run tests in watch mode +bun test --watch + +# Run specific test file +bun test MedicationCard.test.tsx + +# Run tests with coverage +bun test --coverage + +# Run integration tests +bun run test:integration + +# Run E2E tests with Playwright +bun run test:e2e + +# Run E2E tests in UI mode +bun run test:e2e:ui + +# Debug E2E tests +bun run test:e2e:debug + +# Run all tests (unit + integration + e2e) +bun run test:all +``` + +### E2E Testing + +E2E tests use Playwright and are located in `tests/e2e/`. When adding new features: + +```typescript +// Use custom fixtures for authenticated testing +import { test } from './fixtures'; + +test('should perform user action', async ({ adminPage }) => { + // Test implementation with auto-logged-in admin +}); +``` + +See [tests/e2e/README.md](tests/e2e/README.md) for detailed E2E testing guidelines. + +## 📚 Documentation Standards + +### Code Documentation + +- Add JSDoc comments for all public functions +- Document complex algorithms and business logic +- Include examples for utility functions +- Keep comments up-to-date with code changes + +### API Documentation + +- Document all endpoints with examples +- Include request/response schemas +- Specify error codes and messages +- Provide authentication requirements + +### User Documentation + +- Write clear setup instructions +- Include troubleshooting guides +- Provide usage examples +- Keep screenshots current + +## 🔍 Code Review Process + +### Before Requesting Review + +- [ ] All tests pass locally +- [ ] Code follows style guidelines +- [ ] Documentation is updated +- [ ] No console.log statements +- [ ] Secrets are not committed +- [ ] Performance impact considered + +### Review Checklist + +- [ ] Code is readable and well-structured +- [ ] Tests cover new functionality +- [ ] Security implications considered +- [ ] Accessibility requirements met +- [ ] Browser compatibility verified +- [ ] Mobile responsiveness checked + +### Review Response + +- Be open to feedback +- Ask questions for unclear comments +- Address all review comments +- Update documentation if needed +- Test suggested changes + +## 🚀 Release Process + +### Version Numbering + +We follow [Semantic Versioning](https://semver.org/): + +- **MAJOR**: Breaking changes +- **MINOR**: New features (backward compatible) +- **PATCH**: Bug fixes (backward compatible) + +### Release Checklist + +- [ ] Update CHANGELOG.md +- [ ] Update version in package.json +- [ ] Run full test suite +- [ ] Update documentation +- [ ] Create release notes +- [ ] Tag release in Git +- [ ] Deploy to staging +- [ ] Validate staging deployment +- [ ] Deploy to production + +## 🎯 Development Priorities + +### High Priority + +- Bug fixes and security issues +- Performance improvements +- Accessibility enhancements +- Core functionality stability + +### Medium Priority + +- New features and enhancements +- Developer experience improvements +- Documentation updates +- Test coverage improvements + +### Low Priority + +- Code refactoring +- Minor UI improvements +- Non-critical feature requests +- Experimental features + +## 🛠️ Development Tools + +### Required Tools + +- **Node.js 18+** or **Bun 1.0+** +- **Docker and Docker Compose** +- **Git** +- **Code Editor** (VS Code recommended) + +### Recommended Extensions (VS Code) + +- TypeScript and JavaScript Language Features +- ESLint +- Prettier +- Docker +- GitLens +- Thunder Client (for API testing) + +### Useful Commands + +```bash +# Development +bun run dev # Start development server +bun run build # Build for production +bun run preview # Preview production build + +# Quality +bun run lint # Check code quality +bun run lint:fix # Fix linting issues +bun run type-check # TypeScript type checking + +# Testing +bun test # Run tests +bun test:coverage # Run tests with coverage +bun test:watch # Run tests in watch mode + +# Docker +docker compose -f docker/docker-compose.yaml up -d # Start services +docker compose -f docker/docker-compose.yaml logs # View logs +docker compose -f docker/docker-compose.yaml down # Stop services +``` + +## 🆘 Getting Help + +### Documentation + +- [README.md](README.md) - Project overview and setup +- [docs/API.md](docs/API.md) - API documentation +- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) - Deployment guide +- [docs/SECURITY.md](docs/SECURITY.md) - Security guidelines + +### Community + +- **GitHub Issues** - Bug reports and feature requests +- **GitHub Discussions** - General questions and ideas +- **Discord/Slack** - Real-time chat with contributors + +### Support + +- **Email** - development@your-domain.com +- **Professional Support** - Available for enterprise users +- **Consulting** - Custom development and deployment assistance + +## 📄 License + +By contributing to this project, you agree that your contributions will be licensed under the MIT License. + +## 🙏 Recognition + +Contributors will be: + +- Listed in the CHANGELOG.md for their contributions +- Mentioned in release notes for significant features +- Added to the contributors section of README.md +- Eligible for contributor benefits and recognition + +### Types of Contributions Recognized + +- **Code contributions** - Features, bug fixes, improvements +- **Documentation** - Writing, editing, translating +- **Testing** - Bug reports, test writing, QA +- **Design** - UI/UX improvements, graphics, branding +- **Community** - Support, mentoring, evangelism + +Thank you for contributing to the Medication Reminder App! Your efforts help improve healthcare outcomes for users worldwide. 🌟 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..865cd81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,179 @@ +# Medication Reminder App - License + +MIT License + +Copyright (c) 2025 Medication Reminder App Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Third-Party Licenses + +This project includes or uses the following third-party libraries and services: + +### Frontend Dependencies + +#### React (MIT License) +- **Source**: https://github.com/facebook/react +- **License**: MIT +- **Purpose**: User interface framework + +#### Lucide React (ISC License) +- **Source**: https://github.com/lucide-icons/lucide +- **License**: ISC +- **Purpose**: Icon library + +#### React Hook Form (MIT License) +- **Source**: https://github.com/react-hook-form/react-hook-form +- **License**: MIT +- **Purpose**: Form management + +#### Chart.js (MIT License) +- **Source**: https://github.com/chartjs/Chart.js +- **License**: MIT +- **Purpose**: Data visualization + +### Build Tools + +#### Vite (MIT License) +- **Source**: https://github.com/vitejs/vite +- **License**: MIT +- **Purpose**: Build tool and development server + +#### TypeScript (Apache 2.0 License) +- **Source**: https://github.com/microsoft/TypeScript +- **License**: Apache 2.0 +- **Purpose**: Type checking and compilation + +#### Bun (MIT License) +- **Source**: https://github.com/oven-sh/bun +- **License**: MIT +- **Purpose**: JavaScript runtime and package manager + +### Backend Services + +#### CouchDB (Apache 2.0 License) +- **Source**: https://github.com/apache/couchdb +- **License**: Apache 2.0 +- **Purpose**: Database server + +#### Node.js (MIT License) +- **Source**: https://github.com/nodejs/node +- **License**: MIT +- **Purpose**: Server runtime (for production services) + +### External Services + +#### Mailgun +- **Service**: Email delivery +- **Terms**: https://www.mailgun.com/terms/ +- **Privacy**: https://www.mailgun.com/privacy-policy/ + +#### Google OAuth 2.0 +- **Service**: Authentication provider +- **Terms**: https://developers.google.com/terms/ +- **Privacy**: https://policies.google.com/privacy + +### Docker Images + +#### Node.js (Official) +- **Source**: https://hub.docker.com/_/node +- **License**: MIT +- **Purpose**: Base image for application + +#### Nginx (Official) +- **Source**: https://hub.docker.com/_/nginx +- **License**: 2-clause BSD +- **Purpose**: Web server for static files + +#### CouchDB (Official) +- **Source**: https://hub.docker.com/_/couchdb +- **License**: Apache 2.0 +- **Purpose**: Database server + +## License Compliance + +### MIT License Requirements +When redistributing this software: +1. Include the original copyright notice +2. Include the MIT license text +3. Include any third-party license notices + +### Attribution Requirements +When using this software in your own projects: +1. Credit the original authors +2. Link to the original repository +3. Include license information in your documentation + +### Commercial Use +This software is free for commercial use under the MIT license. However: +1. External services (Mailgun, Google OAuth) may have their own terms +2. Consider your data privacy obligations +3. Review compliance requirements for healthcare applications + +## Warranty Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + +This medication reminder application is intended for informational purposes +only and should not be used as a substitute for professional medical advice, +diagnosis, or treatment. Always seek the advice of your physician or other +qualified health provider with any questions you may have regarding a medical +condition. + +## Data Privacy Notice + +This software may process personal health information. When deploying: + +1. **Review Privacy Laws**: Ensure compliance with GDPR, HIPAA, or local regulations +2. **Secure Deployment**: Use encryption, secure credentials, and access controls +3. **Data Handling**: Document data collection, processing, and retention practices +4. **User Consent**: Obtain appropriate consent for data processing +5. **Incident Response**: Have procedures for data breaches or security incidents + +## Medical Disclaimer + +This application is a reminder tool and is not intended to: +- Replace professional medical advice +- Diagnose medical conditions +- Recommend medication changes +- Provide medical treatment + +Users should: +- Consult healthcare providers for medical decisions +- Follow prescribed medication regimens +- Report adverse effects to healthcare providers +- Seek immediate medical attention for emergencies + +## Support and Contact + +For license-related questions: +- **Email**: legal@your-domain.com +- **Issues**: https://github.com/your-username/rxminder/issues +- **Documentation**: https://github.com/your-username/rxminder/docs + +For technical support: +- **Email**: support@your-domain.com +- **Documentation**: [README.md](README.md) +- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md) + +--- + +*Last updated: December 2024* diff --git a/README.md b/README.md new file mode 100644 index 0000000..180dfe7 --- /dev/null +++ b/README.md @@ -0,0 +1,757 @@ +# 💊 RxMinder + +A modern, secure web application for managing medication schedules and reminders. Built with React, TypeScript, CouchDB, and Docker for reliable medication tracking and adherence monitoring. + +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![TypeScript](https://img.shields.io/badge/typescript-5.8.2-blue.svg) +![React](https://img.shields.io/badge/react-19.1.1-blue.svg) +![Docker](https://img.shields.io/badge/docker-ready-blue.svg) + +## ✨ Features + +### 🔐 **Authentication & Security** + +- **Email/Password Authentication** with secure password hashing (bcrypt) +- **OAuth Integration** (Google, GitHub) for social login +- **Email Verification** for account activation +- **Password Reset** functionality with secure tokens +- **Admin Interface** for user management +- **Role-based Access Control** (User, Admin) + +### 💊 **Medication Management** + +- **Add/Edit/Delete Medications** with dosage and frequency +- **Flexible Scheduling** (Daily, Twice/Three times daily, Custom intervals) +- **Visual Medication Cards** with custom icons +- **Medication History** tracking + +### ⏰ **Reminder System** + +- **Smart Scheduling** based on medication frequency +- **Dose Tracking** (Taken, Missed, Upcoming) +- **Custom Reminders** with personalized messages +- **Adherence Statistics** and progress monitoring + +### 📊 **Analytics & Insights** + +- **Daily Adherence Statistics** with visual charts +- **Medication-specific Analytics** (taken vs missed doses) +- **Progress Tracking** over time +- **Export Capabilities** for healthcare providers + +### 🎨 **User Experience** + +- **Responsive Design** for mobile and desktop +- **Dark/Light Theme** support +- **Intuitive Interface** with modern design +- **Onboarding Flow** for new users +- **Avatar Customization** with image upload + +## 🏗️ Architecture + +### **Frontend Stack** + +- **React 19** with TypeScript +- **Vite** for fast development and building +- **Modern CSS** with responsive design +- **Component-based Architecture** + +### **Backend Services** + +- **CouchDB** for document-based data storage +- **Mailgun** for email delivery (verification, password reset) +- **bcrypt** for secure password hashing +- **JWT-like** token system for authentication + +### **Infrastructure** + +- **Docker & Docker Compose** for containerization +- **Nginx** for production static file serving +- **Multi-stage Builds** for optimized images +- **Health Checks** for service monitoring + +### **Development Tools** + +- **TypeScript** for type safety and modern JavaScript features +- **ESLint** for code quality and consistent style +- **Prettier** for automated code formatting +- **Pre-commit hooks** for automated quality checks +- **Bun** for fast package management and development +- **Environment-based Configuration** for flexible deployments + +## 🚀 Quick Start + +### **Prerequisites** + +- [Docker](https://docker.com) and Docker Compose +- [Bun](https://bun.sh) (for local development) +- [Git](https://git-scm.com) + +### **1. Clone and Setup** + +```bash +git clone +cd meds +./setup.sh + +# Validate configuration (optional) +./validate-env.sh +``` + +### **2. Configure Environment** + +```bash +# Copy the template and customize +cp .env.example .env + +# Edit .env with your credentials +nano .env +``` + +### **3. Deploy** + +```bash +# Quick deployment +./deploy.sh + +# Or manual Docker Compose +docker compose up -d +``` + +### **4. Access the Application** + +- **Frontend**: http://localhost:8080 +- **CouchDB Admin**: http://localhost:5984/\_utils +- **Default Admin**: `admin@localhost` / `change-this-secure-password` + +## 🔧 Development + +### **Local Development** + +```bash +# Install dependencies +bun install + +# Start development server +bun run dev + +# Run with real CouchDB (Docker) +docker compose up -d couchdb +VITE_COUCHDB_URL=http://localhost:5984 bun run dev +``` + +### **Code Quality** + +This project includes comprehensive code quality tools and pre-commit hooks. See [`docs/development/CODE_QUALITY.md`](docs/development/CODE_QUALITY.md) for detailed documentation. + +```bash +# Format code +bun run format + +# Check formatting +bun run format:check + +# Lint code +bun run lint + +# Fix lint issues +bun run lint:fix + +# Type checking +bun run type-check + +# Run pre-commit checks +bun run pre-commit + +# Setup pre-commit hooks (one-time) +./scripts/setup-pre-commit.sh +``` + +**Automatic Quality Checks**: Pre-commit hooks automatically format code, run linting, type checking, and security scans on every commit. + +### **Testing** + +```bash +# Run tests +bun run test + +# Run specific test file +bun run test auth.integration.test.ts +``` + +## 🔐 Security & Configuration + +### **Environment Variables** + +#### **Required Variables** + +```bash +# CouchDB Configuration +COUCHDB_USER=admin +COUCHDB_PASSWORD=your-secure-password +VITE_COUCHDB_URL=http://localhost:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=your-secure-password +``` + +#### **Optional Variables** + +```bash +# Mailgun (for email features) +MAILGUN_API_KEY=your-mailgun-api-key +MAILGUN_DOMAIN=your-domain.com +MAILGUN_FROM_EMAIL=noreply@your-domain.com + +# Production Settings +NODE_ENV=production +``` + +### **Security Best Practices** + +1. **🔒 Never commit `.env` files** - Already in `.gitignore` +2. **🛡️ Use strong passwords** - Minimum 8 characters with mixed case, numbers, symbols +3. **🔄 Rotate credentials regularly** - Especially in production +4. **📧 Verify email configuration** - Test Mailgun setup before production +5. **🔍 Monitor logs** - Check Docker logs for security events +6. **🚪 Limit access** - Use firewall rules for production deployments + +### **Credential Management Methods** + +#### **Development** + +```bash +# Method 1: .env file (recommended for local dev) +cp .env.example .env +# Edit with your values + +# Method 2: Shell environment +export COUCHDB_PASSWORD="secure-password" +export MAILGUN_API_KEY="key-123..." +``` + +#### **Production** + +```bash +# Method 1: Secure deployment script +./deploy.sh production + +# Method 2: CI/CD with environment variables +# Set in GitHub Actions, GitLab CI, etc. + +# Method 3: External secrets management +# AWS Secrets Manager, Azure Key Vault, etc. +``` + +#### **Docker Deployment** + +```bash +# Using .env file +docker compose --env-file .env.production up -d + +# Using environment variables +COUCHDB_PASSWORD="secure-password" docker compose up -d +``` + +## 📁 Project Structure + +``` +meds/ +├── 📄 README.md # This documentation +├── package.json # Dependencies and scripts +├── ⚙️ vite.config.ts # Build configuration +├── 📝 tsconfig.json # TypeScript configuration +├── 🎨 index.html # Entry point +├── 🚀 deploy.sh # Secure deployment script +├── 🔧 setup.sh # Development setup script +├── 🌱 seed-production.js # Database seeding +├── 🧪 test-production.js # Production testing +├── 🔒 .env.example # Environment template +│ +├── 📁 docker/ # Container configuration +│ ├── 🐳 Dockerfile # Multi-stage Docker build +│ ├── 🐳 docker-compose.yaml # Service orchestration +│ ├── 🌐 nginx.conf # Production web server config +│ └── 🚫 .dockerignore # Docker ignore patterns +│ +├── 📁 components/ # React components +│ ├── 🔐 AuthPage.tsx # Login/register interface +│ ├── 👑 AdminInterface.tsx # Admin user management +│ ├── 💊 AddMedicationModal.tsx # Medication creation +│ ├── ⏰ ReminderCard.tsx # Reminder display +│ ├── 📊 StatsModal.tsx # Analytics dashboard +│ └── ... # Other UI components +│ +├── 📁 services/ # Business logic & APIs +│ ├── 🗄️ couchdb.ts # Mock database service +│ ├── 🗄️ couchdb.production.ts # Real CouchDB service +│ ├── 🏭 couchdb.factory.ts # Service factory +│ ├── 📧 mailgun.service.ts # Email delivery +│ ├── 📧 mailgun.config.ts # Email configuration +│ ├── 🌱 database.seeder.ts # Data seeding +│ └── 📁 auth/ # Authentication services +│ ├── 🔐 auth.service.ts # Core auth logic +│ ├── ✉️ emailVerification.service.ts +│ └── 📁 __tests__/ # Test suites +│ +├── 📁 contexts/ # React context providers +│ └── 👤 UserContext.tsx # User state management +│ +├── 📁 hooks/ # Custom React hooks +│ ├── 💾 useLocalStorage.ts # Persistent storage +│ ├── ⚙️ useSettings.ts # User preferences +│ └── 🎨 useTheme.ts # Theme management +│ +└── 📁 utils/ # Utility functions + └── ⏰ schedule.ts # Reminder scheduling +``` + +## 🎯 API Reference + +### **Authentication Endpoints** + +#### **Register User** + +```typescript +authService.register(email: string, password: string, username?: string) +// Returns: { user: User, verificationToken: EmailVerificationToken } +``` + +#### **Login User** + +```typescript +authService.login({ email: string, password: string }); +// Returns: { user: User, accessToken: string, refreshToken: string } +``` + +#### **OAuth Login** + +```typescript +authService.loginWithOAuth(provider: 'google' | 'github', userData: OAuthUserData) +// Returns: { user: User, accessToken: string, refreshToken: string } +``` + +#### **Change Password** + +```typescript +authService.changePassword(userId: string, currentPassword: string, newPassword: string) +// Returns: { success: boolean, message: string } +``` + +### **Database Operations** + +#### **User Management** + +```typescript +dbService.saveUser(user: User): Promise +dbService.findUserByEmail(email: string): Promise +dbService.updateUser(userId: string, updates: Partial): Promise +dbService.deleteUser(userId: string): Promise +``` + +#### **Medication Management** + +```typescript +dbService.saveMedication(medication: Medication): Promise +dbService.getMedications(userId: string): Promise +dbService.updateMedication(medicationId: string, updates: Partial): Promise +dbService.deleteMedication(medicationId: string): Promise +``` + +#### **Reminder & Dose Tracking** + +```typescript +dbService.saveReminder(reminder: CustomReminder): Promise +dbService.getReminders(userId: string): Promise +dbService.saveTakenDose(dose: TakenDose): Promise +dbService.getTakenDoses(userId: string, date?: string): Promise +``` + +## 🐳 Docker Reference + +### **Build Images** + +```bash +# Build all services +docker compose build + +# Build specific service +docker compose build frontend + +# Build with no cache +docker compose build --no-cache +``` + +### **Manage Services** + +```bash +# Start all services +docker compose up -d + +# Start specific service +docker compose up -d couchdb + +# Stop all services +docker compose down + +# View logs +docker compose logs +docker compose logs frontend +``` + +### **Database Management** + +```bash +# Access CouchDB container +docker compose exec couchdb bash + +# Backup database +docker compose exec couchdb curl -X GET http://admin:password@localhost:5984/users/_all_docs?include_docs=true + +# Restore database +# Use CouchDB Fauxton interface or curl commands +``` + +## 🧪 Testing & Quality Assurance + +### **Development Testing** + +```bash +# Run all unit tests +bun run test + +# Run tests in watch mode +bun run test:watch + +# Run with coverage +bun run test:coverage + +# Run integration tests +bun run test:integration + +# Run E2E tests with Playwright +bun run test:e2e + +# Run E2E tests in UI mode +bun run test:e2e:ui + +# Debug E2E tests +bun run test:e2e:debug + +# Run all tests (unit + integration + e2e) +bun run test:all +``` + +### **Testing Structure** + +- **Unit Tests**: Jest-based tests for individual functions and components +- **Integration Tests**: Production environment validation and service testing +- **E2E Tests**: Playwright-based full user journey testing across browsers +- **Manual Tests**: Browser console debugging scripts + +See [tests/README.md](tests/README.md) for detailed testing documentation. + +### **Test Production Environment** + +```bash +# Run comprehensive production tests +bun test-production.js + +# Manual testing checklist +./deploy.sh # Deploy environment +# Visit http://localhost:8080 +# Test user registration/login +# Test admin interface +# Test medication management +# Test password change +# Verify data persistence +``` + +### **Performance Testing** + +```bash +# Check service health +docker compose ps +curl -f http://localhost:5984/_up +curl -f http://localhost:8080 + +# Monitor resource usage +docker stats +``` + +### **Security Testing** + +```bash +# Check for vulnerable dependencies +bun audit + +# Validate environment configuration +./deploy.sh --dry-run + +# Test authentication flows +# - Registration with weak passwords +# - Login with wrong credentials +# - Access admin without proper role +``` + +## 🚀 Deployment Guide + +### **Development Deployment** + +```bash +# Quick local setup +./setup.sh +``` + +### **Production Deployment** + +```bash +# Secure production deployment +./deploy.sh production +``` + +### **Cloud Deployment** + +#### **AWS EC2** + +```bash +# 1. Launch EC2 instance with Docker +# 2. Clone repository +git clone +cd meds + +# 3. Configure environment +cp .env.example .env +# Edit .env with production values + +# 4. Deploy +./deploy.sh production +``` + +#### **Google Cloud Run** + +```bash +# Build and push image +gcloud builds submit --tag gcr.io/PROJECT-ID/meds-app + +# Deploy with environment variables +gcloud run deploy meds-app \ + --image gcr.io/PROJECT-ID/meds-app \ + --set-env-vars COUCHDB_URL=your-couchdb-url \ + --set-env-vars MAILGUN_API_KEY=your-key +``` + +#### **Kubernetes (Template-Based)** + +```bash +# 1. Copy and configure environment +cp .env.example .env +# Edit .env with your secure credentials + +# 2. Deploy with templates (recommended) +./scripts/k8s-deploy-template.sh deploy + +# Alternative: Manual deployment +# Create secrets manually +kubectl create secret generic meds-secrets \ + --from-literal=couchdb-password=secure-password \ + --from-literal=mailgun-api-key=your-key + +# Apply manifests +kubectl apply -f k8s/ +``` + +## 🔍 Troubleshooting + +### **Common Issues** + +#### **Environment Variables Not Loading** + +```bash +# Check .env file exists and is properly formatted +cat .env + +# Verify Docker Compose uses env file +docker compose config +``` + +#### **CouchDB Connection Issues** + +```bash +# Check CouchDB health +curl -u admin:password http://localhost:5984/_up + +# Verify credentials +docker compose logs couchdb + +# Reset database +docker compose down +docker volume rm meds_couchdb-data +docker compose up -d +``` + +#### **Frontend Build Failures** + +```bash +# Clear node modules and reinstall +rm -rf node_modules bun.lockb +bun install + +# Check for TypeScript errors +bun run type-check + +# Build with verbose output +bun run build --verbose +``` + +#### **Email Not Sending** + +```bash +# Verify Mailgun configuration +echo $MAILGUN_API_KEY +echo $MAILGUN_DOMAIN + +# Check Mailgun service logs +docker compose logs frontend | grep -i mailgun + +# Test Mailgun API directly +curl -s --user 'api:YOUR_API_KEY' \ + https://api.mailgun.net/v3/YOUR_DOMAIN/messages \ + -F from='test@YOUR_DOMAIN' \ + -F to='you@example.com' \ + -F subject='Test' \ + -F text='Testing' +``` + +### **Performance Issues** + +```bash +# Check resource usage +docker stats + +# Optimize Docker images +docker system prune -a + +# Monitor application performance +docker compose logs --tail=100 frontend +``` + +### **Debug Mode** + +```bash +# Run with debug logging +DEBUG=* docker compose up + +# Access container for debugging +docker compose exec frontend sh +docker compose exec couchdb bash +``` + +## 📚 Documentation + +### **Complete Documentation Index** + +For comprehensive documentation, visit **[`docs/README.md`](docs/README.md)** which includes: + +#### 🏗️ Architecture & Design + +- [Project Structure](docs/architecture/PROJECT_STRUCTURE.md) - Codebase organization +- [Template Approach](docs/architecture/TEMPLATE_APPROACH.md) - Design philosophy + +#### 🚀 Setup & Configuration + +- [Complete Template Configuration](docs/setup/COMPLETE_TEMPLATE_CONFIGURATION.md) - Full setup guide +- [Setup Complete](docs/setup/SETUP_COMPLETE.md) - Post-setup verification + +#### 💻 Development + +- [API Documentation](docs/development/API.md) - REST API endpoints +- [Code Quality](docs/development/CODE_QUALITY.md) - Quality standards & tools +- [Application Security](docs/development/APPLICATION_SECURITY.md) - App security practices + +#### 🚢 Deployment + +- [Deployment Guide](docs/deployment/DEPLOYMENT.md) - General deployment +- [Docker Configuration](docs/deployment/DOCKER_IMAGE_CONFIGURATION.md) - Docker setup +- [Gitea Setup](docs/deployment/GITEA_SETUP.md) - CI/CD configuration + +#### 🔄 Migration Guides + +- [NodeJS Pre-commit Migration](docs/migration/NODEJS_PRECOMMIT_MIGRATION.md) - Modern git hooks +- [Buildx Migration](docs/migration/BUILDX_MIGRATION.md) - Docker improvements + +## 📚 Additional Resources + +### **Documentation** + +- [CouchDB Documentation](https://docs.couchdb.org/) +- [Mailgun API Reference](https://documentation.mailgun.com/) +- [Docker Compose Reference](https://docs.docker.com/compose/) +- [React Documentation](https://react.dev/) + +### **Development Tools** + +- [Bun Documentation](https://bun.sh/docs) +- [Vite Documentation](https://vitejs.dev/) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/) + +### **Security Resources** + +- [OWASP Security Guidelines](https://owasp.org/) +- [Docker Security Best Practices](https://docs.docker.com/engine/security/) +- [CouchDB Security](https://docs.couchdb.org/en/stable/intro/security.html) + +## 🤝 Contributing + +1. **Fork the repository** +2. **Create a feature branch**: `git checkout -b feature/amazing-feature` +3. **Commit changes**: `git commit -m 'Add amazing feature'` +4. **Push to branch**: `git push origin feature/amazing-feature` +5. **Open a Pull Request** + +### **Development Workflow** + +```bash +# Setup development environment +./setup.sh + +# Make changes and test +bun run dev +bun run lint +bun run type-check + +# Test in production environment +./deploy.sh +bun test-production.js + +# Submit pull request +``` + +## � Documentation + +### **Project Documentation** + +- **[Code Quality Guide](docs/CODE_QUALITY.md)** - Code formatting, linting, and pre-commit hooks setup +- **[Security Guide](docs/SECURITY.md)** - Security best practices and configuration +- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment instructions +- **[API Documentation](docs/API.md)** - Complete API reference +- **[Contributing Guide](CONTRIBUTING.md)** - Development guidelines and contribution process +- **[License](LICENSE)** - MIT license and third-party attributions +- **[Changelog](CHANGELOG.md)** - Version history and release notes + +## �📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🙏 Acknowledgments + +- **CouchDB Team** for the robust database system +- **Mailgun** for reliable email delivery +- **React Team** for the excellent frontend framework +- **Docker Team** for containerization technology +- **Bun Team** for the fast JavaScript runtime + +--- + +**Built with ❤️ for better medication adherence and health outcomes.** + +For support, please open an issue on GitHub or contact the development team. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b5be695 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,194 @@ +# 🔐 Security Configuration Guide for RxMinder + +This guide outlines the security configurations in RxMinder and how to properly secure your deployment. + +> **📋 Related Documentation**: For application-level security practices (password requirements, authentication, etc.), see [`docs/development/APPLICATION_SECURITY.md`](docs/development/APPLICATION_SECURITY.md) + +## ⚠️ Critical Security Updates + +We use a template-based approach with environment variables for secure, user-friendly credential management. + +## 🔑 Template-Based Configuration + +### Kubernetes Deployment + +**Files**: `k8s/*.yaml.template` + +RxMinder uses Kubernetes template files that automatically substitute environment variables. No manual base64 encoding required! + +**Template files:** + +- `k8s/couchdb-secret.yaml.template` - Database credentials +- `k8s/ingress.yaml.template` - Ingress configuration +- `k8s/configmap.yaml.template` - Application configuration +- `k8s/frontend-deployment.yaml.template` - Frontend deployment + +**Example secret template:** + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: couchdb-secret + labels: + app: ${APP_NAME:-rxminder} +type: Opaque +stringData: + username: ${COUCHDB_USER:-admin} + password: ${COUCHDB_PASSWORD:-change-this-secure-password} +``` + +### Environment Variables + +**File**: `.env` + +```env +# Application Name (used in Kubernetes labels) +APP_NAME=rxminder + +# Database Credentials (automatically substituted in templates) +COUCHDB_USER=admin +COUCHDB_PASSWORD=your-very-secure-password +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=your-very-secure-password + +# Kubernetes Configuration +INGRESS_HOST=rxminder.yourdomain.com +``` + +## 🚀 Template-Based Deployment + +### Quick Start + +1. **Copy environment template:** + + ```bash + cp .env.example .env + ``` + +2. **Update .env with your secure credentials:** + + ```bash + # Edit .env with your secure passwords and configuration + nano .env + ``` + +3. **Deploy with templates:** + ```bash + ./scripts/k8s-deploy-template.sh deploy + ``` + +The deployment script automatically: + +- ✅ Loads environment variables from `.env` +- ✅ Substitutes variables in template files +- ✅ Applies resources in correct dependency order +- ✅ Runs database seeding +- ✅ Shows deployment status + +## 🛡️ Security Best Practices + +### 1. **Strong Passwords** + +- Use passwords with at least 16 characters +- Include uppercase, lowercase, numbers, and symbols +- Use a password manager to generate unique passwords + +### 2. **Environment-Specific Credentials** + +- **Development**: Use different credentials than production +- **Staging**: Use different credentials than production +- **Production**: Use strong, unique credentials + +### 3. **Credential Rotation** + +- Rotate database credentials regularly +- Update Kubernetes secrets using `kubectl` +- Update Docker environment variables + +### 4. **Secret Management** + +- Never commit actual credentials to version control +- Use Kubernetes secrets for production deployments +- Consider external secret management (HashiCorp Vault, etc.) + +## 🔄 Updating Credentials + +### Kubernetes Environment + +```bash +# Create new secret with secure credentials +kubectl create secret generic couchdb-secret \ + --from-literal=username=your-secure-username \ + --from-literal=password=your-very-secure-password \ + --dry-run=client -o yaml | kubectl apply -f - + +# Restart pods to pick up new credentials +kubectl rollout restart statefulset/couchdb +kubectl rollout restart deployment/frontend +``` + +### Docker Environment + +````bash +### Docker Environment + +```bash +# Update environment variables and restart containers +export COUCHDB_PASSWORD="your-very-secure-password" +export VITE_COUCHDB_PASSWORD="your-very-secure-password" +docker compose down && docker compose up -d +```` + +## 🔄 CI/CD Security + +### GitHub Actions / Gitea Workflows + +Set these secrets in your repository settings: + +- `VITE_COUCHDB_PASSWORD`: Your production CouchDB password +- `GITEA_TOKEN` / `GITHUB_TOKEN`: For container registry authentication + +**Important**: CI/CD workflows use secure fallback values but should use repository secrets for production builds. + +### Test Environments + +Test databases use secure passwords by default: + +- CI containers: `test-secure-password` +- End-to-end tests: Use dedicated test credentials (acceptable for testing) + +## ✅ Security Checklist + +``` + +## ⚡ Quick Security Checklist + +- [ ] Changed default admin password in `k8s/couchdb-secret.yaml` +- [ ] Updated `.env` file with secure credentials +- [ ] Used different passwords for each environment +- [ ] Credentials are not in version control (in `.gitignore`) +- [ ] Reviewed all scripts for hardcoded values +- [ ] Configured proper network policies (if using Kubernetes) +- [ ] Set up TLS/SSL for production deployments + +## 🚨 Emergency Response + +If credentials are compromised: + +1. **Immediately** change passwords in all environments +2. Rotate Kubernetes secrets +3. Review access logs +4. Update any applications using the old credentials +5. Consider rotating container registry credentials if needed + +## 📚 Additional Resources + +- [CouchDB Security Best Practices](https://docs.couchdb.org/en/stable/intro/security.html) +- [Kubernetes Secrets Management](https://kubernetes.io/docs/concepts/configuration/secret/) +- [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) + +--- + +**Remember**: Security is an ongoing process, not a one-time setup. Regularly review and update your security configurations. +``` diff --git a/banner.jpeg b/banner.jpeg new file mode 100644 index 0000000..82a03ba Binary files /dev/null and b/banner.jpeg differ diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..57b7b64 --- /dev/null +++ b/bun.lock @@ -0,0 +1,2611 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "dependencies": { + "bcryptjs": "^3.0.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "uuid": "^12.0.0", + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@secretlint/node": "^11.2.3", + "@secretlint/secretlint-rule-preset-recommend": "^11.2.3", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@types/jest": "^30.0.0", + "@types/node": "^22.14.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.42.0", + "@typescript-eslint/parser": "^8.42.0", + "dockerfilelint": "^1.8.0", + "eclint": "^2.8.1", + "eslint": "^9.35.0", + "eslint-define-config": "^2.1.0", + "eslint-plugin-react-hooks": "^5.2.0", + "husky": "^9.1.7", + "jest": "^30.1.3", + "jest-environment-jsdom": "^30.1.2", + "lint-staged": "^16.1.6", + "markdownlint-cli2": "^0.18.1", + "prettier": "^3.6.2", + "shelljs": "^0.10.0", + "ts-jest": "^29.4.1", + "typescript": "^5.9.2", + "vite": "^7.1.4", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + + "@azu/format-text": ["@azu/format-text@1.0.2", "", {}, "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg=="], + + "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], + + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "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" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], + + "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + + "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], + + "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], + + "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], + + "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], + + "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], + + "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], + + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.8.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="], + + "@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "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" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "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" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jest/console": ["@jest/console@30.1.2", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "jest-message-util": "30.1.0", "jest-util": "30.0.5", "slash": "^3.0.0" } }, "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw=="], + + "@jest/core": ["@jest/core@30.1.3", "", { "dependencies": { "@jest/console": "30.1.2", "@jest/pattern": "30.0.1", "@jest/reporters": "30.1.3", "@jest/test-result": "30.1.3", "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@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.5", "jest-config": "30.1.3", "jest-haste-map": "30.1.0", "jest-message-util": "30.1.0", "jest-regex-util": "30.0.1", "jest-resolve": "30.1.3", "jest-resolve-dependencies": "30.1.3", "jest-runner": "30.1.3", "jest-runtime": "30.1.3", "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "jest-validate": "30.1.0", "jest-watcher": "30.1.3", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ=="], + + "@jest/diff-sequences": ["@jest/diff-sequences@30.0.1", "", {}, "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw=="], + + "@jest/environment": ["@jest/environment@30.1.2", "", { "dependencies": { "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5" } }, "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w=="], + + "@jest/environment-jsdom-abstract": ["@jest/environment-jsdom-abstract@30.1.2", "", { "dependencies": { "@jest/environment": "30.1.2", "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/jsdom": "^21.1.7", "@types/node": "*", "jest-mock": "30.0.5", "jest-util": "30.0.5" }, "peerDependencies": { "canvas": "^3.0.0", "jsdom": "*" }, "optionalPeers": ["canvas"] }, "sha512-u8kTh/ZBl97GOmnGJLYK/1GuwAruMC4hoP6xuk/kwltmVWsA9u/6fH1/CsPVGt2O+Wn2yEjs8n1B1zZJ62Cx0w=="], + + "@jest/expect": ["@jest/expect@30.1.2", "", { "dependencies": { "expect": "30.1.2", "jest-snapshot": "30.1.2" } }, "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA=="], + + "@jest/expect-utils": ["@jest/expect-utils@30.1.2", "", { "dependencies": { "@jest/get-type": "30.1.0" } }, "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A=="], + + "@jest/fake-timers": ["@jest/fake-timers@30.1.2", "", { "dependencies": { "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" } }, "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA=="], + + "@jest/get-type": ["@jest/get-type@30.1.0", "", {}, "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA=="], + + "@jest/globals": ["@jest/globals@30.1.2", "", { "dependencies": { "@jest/environment": "30.1.2", "@jest/expect": "30.1.2", "@jest/types": "30.0.5", "jest-mock": "30.0.5" } }, "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A=="], + + "@jest/pattern": ["@jest/pattern@30.0.1", "", { "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" } }, "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA=="], + + "@jest/reporters": ["@jest/reporters@30.1.3", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "30.1.2", "@jest/test-result": "30.1.3", "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@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.1.0", "jest-util": "30.0.5", "jest-worker": "30.1.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w=="], + + "@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="], + + "@jest/snapshot-utils": ["@jest/snapshot-utils@30.1.2", "", { "dependencies": { "@jest/types": "30.0.5", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" } }, "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw=="], + + "@jest/source-map": ["@jest/source-map@30.0.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "callsites": "^3.1.0", "graceful-fs": "^4.2.11" } }, "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg=="], + + "@jest/test-result": ["@jest/test-result@30.1.3", "", { "dependencies": { "@jest/console": "30.1.2", "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" } }, "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ=="], + + "@jest/test-sequencer": ["@jest/test-sequencer@30.1.3", "", { "dependencies": { "@jest/test-result": "30.1.3", "graceful-fs": "^4.2.11", "jest-haste-map": "30.1.0", "slash": "^3.0.0" } }, "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w=="], + + "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], + + "@jest/types": ["@jest/types@30.0.5", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + + "@playwright/test": ["@playwright/test@1.55.0", "", { "dependencies": { "playwright": "1.55.0" }, "bin": { "playwright": "cli.js" } }, "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="], + + "@secretlint/config-loader": ["@secretlint/config-loader@11.2.3", "", { "dependencies": { "@secretlint/profiler": "11.2.3", "@secretlint/resolver": "11.2.3", "@secretlint/types": "11.2.3", "ajv": "^8.17.1", "debug": "^4.4.1", "rc-config-loader": "^4.1.3" } }, "sha512-kTkNZGOEOvhONeUIHu3FFKfiIyFOaapvL6JEC60ZOZ77CDH2y0HespcgJa8MT9NsAwth9Y7j+EwWpFOb4ynotQ=="], + + "@secretlint/core": ["@secretlint/core@11.2.3", "", { "dependencies": { "@secretlint/profiler": "11.2.3", "@secretlint/types": "11.2.3", "debug": "^4.4.1", "structured-source": "^4.0.0" } }, "sha512-pfPFy2nKGCRwEQFsWUQz/apcEE43PqAoCPPNNnj2xYYSCs2VRd/yUrFzdibIiY3Ww9SK2a6fHC8luliLGkZ2Ew=="], + + "@secretlint/formatter": ["@secretlint/formatter@11.2.3", "", { "dependencies": { "@secretlint/resolver": "11.2.3", "@secretlint/types": "11.2.3", "@textlint/linter-formatter": "^15.2.2", "@textlint/module-interop": "^15.2.2", "@textlint/types": "^15.2.2", "chalk": "^5.6.0", "debug": "^4.4.1", "pluralize": "^8.0.0", "strip-ansi": "^7.1.0", "table": "^6.9.0", "terminal-link": "^4.0.0" } }, "sha512-6R+kwSDL9rnuy4TixNM+f1bbRkl1yOzTkpEVkchR9bZWXANdPXMQyLubCgohPYUSD4eib9ObvcndvS81p8jZPQ=="], + + "@secretlint/node": ["@secretlint/node@11.2.3", "", { "dependencies": { "@secretlint/config-loader": "11.2.3", "@secretlint/core": "11.2.3", "@secretlint/formatter": "11.2.3", "@secretlint/profiler": "11.2.3", "@secretlint/source-creator": "11.2.3", "@secretlint/types": "11.2.3", "debug": "^4.4.1", "p-map": "^7.0.3" } }, "sha512-8mYOdHVwuMNXq0bwwC309xUmmgisMjIypJl5z0U1TQqWZoqRK+N1OQ7vHKZfzA/lnEIm4MAqKclr2rLDNaapfQ=="], + + "@secretlint/profiler": ["@secretlint/profiler@11.2.3", "", {}, "sha512-CpXYj8LhwsRhZ7HYIapIVROoJ0wJVnmKPX/HnpyJzyGSKZoqbsW+VsAF4npspfZBjP6uLBZKfQHvOitQFOqOPg=="], + + "@secretlint/resolver": ["@secretlint/resolver@11.2.3", "", {}, "sha512-f2b9wgLS1GwyRo4E459Q3n534KC0azYmMFRSwnRT65HDtZCci0BGv/m2AUCvor8+q2oSePrCC6NZnLwJDzm6Kg=="], + + "@secretlint/secretlint-rule-preset-recommend": ["@secretlint/secretlint-rule-preset-recommend@11.2.3", "", {}, "sha512-tpjzerm1KedNnf8xkittHW0sIeCARH8+JFzgQDslQNtbqJ2MSPEs5e5VcJ0i2ZdHVhq1LC1osm88RmwV4k65yA=="], + + "@secretlint/source-creator": ["@secretlint/source-creator@11.2.3", "", { "dependencies": { "@secretlint/types": "11.2.3", "istextorbinary": "^9.5.0" } }, "sha512-x555rAk0BuVKbYVidoxWMV/YksGtLCfHDn0lSQPNUlD1SXTyP4kmscSfBMgp3g5HC1TK8fwT3oN1WRnivY92Fg=="], + + "@secretlint/types": ["@secretlint/types@11.2.3", "", {}, "sha512-zD7+FP700caEOErMp5ecds9twTs/YfmjwEQGJ44k0TnGmeZOovB/EoxAcYIeBXk46mia1MJh9dY+oc/Pu3EFrA=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + + "@testing-library/dom": ["@testing-library/dom@9.3.4", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.8.0", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ=="], + + "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], + + "@textlint/ast-node-types": ["@textlint/ast-node-types@15.2.2", "", {}, "sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA=="], + + "@textlint/linter-formatter": ["@textlint/linter-formatter@15.2.2", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.2.2", "@textlint/resolver": "15.2.2", "@textlint/types": "15.2.2", "chalk": "^4.1.2", "debug": "^4.4.1", "js-yaml": "^3.14.1", "lodash": "^4.17.21", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg=="], + + "@textlint/module-interop": ["@textlint/module-interop@15.2.2", "", {}, "sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA=="], + + "@textlint/resolver": ["@textlint/resolver@15.2.2", "", {}, "sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q=="], + + "@textlint/types": ["@textlint/types@15.2.2", "", { "dependencies": { "@textlint/ast-node-types": "15.2.2" } }, "sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + + "@types/jest": ["@types/jest@30.0.0", "", { "dependencies": { "expect": "^30.0.0", "pretty-format": "^30.0.0" } }, "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA=="], + + "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="], + + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + + "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.42.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.42.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.42.0", "@typescript-eslint/types": "^8.42.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.42.0", "", { "dependencies": { "@typescript-eslint/types": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0" } }, "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.42.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.42.0", "", { "dependencies": { "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/utils": "8.42.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.42.0", "", {}, "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.42.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.42.0", "@typescript-eslint/tsconfig-utils": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.42.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.42.0", "", { "dependencies": { "@typescript-eslint/types": "8.42.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-colors": ["ansi-colors@1.1.0", "", { "dependencies": { "ansi-wrap": "^0.1.0" } }, "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA=="], + + "ansi-cyan": ["ansi-cyan@0.1.1", "", { "dependencies": { "ansi-wrap": "0.1.0" } }, "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A=="], + + "ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], + + "ansi-gray": ["ansi-gray@0.1.1", "", { "dependencies": { "ansi-wrap": "0.1.0" } }, "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw=="], + + "ansi-red": ["ansi-red@0.1.1", "", { "dependencies": { "ansi-wrap": "0.1.0" } }, "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow=="], + + "ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "ansi-wrap": ["ansi-wrap@0.1.0", "", {}, "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "append-buffer": ["append-buffer@1.0.2", "", { "dependencies": { "buffer-equal": "^1.0.0" } }, "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "arr-diff": ["arr-diff@4.0.0", "", {}, "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA=="], + + "arr-flatten": ["arr-flatten@1.1.0", "", {}, "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg=="], + + "arr-union": ["arr-union@3.1.0", "", {}, "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-differ": ["array-differ@1.0.0", "", {}, "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ=="], + + "array-slice": ["array-slice@0.2.3", "", {}, "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q=="], + + "array-union": ["array-union@1.0.2", "", { "dependencies": { "array-uniq": "^1.0.1" } }, "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng=="], + + "array-uniq": ["array-uniq@1.0.3", "", {}, "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q=="], + + "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + + "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axios": ["axios@0.18.1", "", { "dependencies": { "follow-redirects": "1.5.10", "is-buffer": "^2.0.2" } }, "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g=="], + + "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], + + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "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 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], + + "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="], + + "bignumber.js": ["bignumber.js@2.4.0", "", {}, "sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ=="], + + "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], + + "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.25.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg=="], + + "bs-logger": ["bs-logger@0.2.6", "", { "dependencies": { "fast-json-stable-stringify": "2.x" } }, "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog=="], + + "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + + "buffer-equal": ["buffer-equal@1.0.1", "", {}, "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg=="], + + "buffer-equals": ["buffer-equals@1.0.4", "", {}, "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "buffered-spawn": ["buffered-spawn@3.3.2", "", { "dependencies": { "cross-spawn": "^4.0.0" } }, "sha512-YVdiyWEbFCH+lu3USRFoH6UtvS3mr/e/obxZNbOkbbL3heLEUYb3YpTjKUQFWt5d3k9ZILabY8Kh2pp+i4SQqg=="], + + "bufferstreams": ["bufferstreams@2.0.1", "", { "dependencies": { "readable-stream": "^2.3.6" } }, "sha512-ZswyIoBfFb3cVDsnZLLj2IDJ/0ppYdil/v2EGlZXvoefO689FokEmFEldhN5dV7R2QBxFneqTJOMIpfqhj+n0g=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001741", "", {}, "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw=="], + + "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "checkstyle-formatter": ["checkstyle-formatter@1.1.0", "", { "dependencies": { "xml-escape": "^1.0.0" } }, "sha512-mak+5ooX5cDFBBIhsR+NqxoQ9+JQRqupr49G2PiUYXKn8OntoI9osjhECaScrzqq1l4phuRmK1VlMdxHdpwZvg=="], + + "ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "cjs-module-lexer": ["cjs-module-lexer@2.1.0", "", {}, "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@1.1.0", "", { "dependencies": { "slice-ansi": "^1.0.0", "string-width": "^2.0.0" } }, "sha512-bAtZo0u82gCfaAGfSNxUdTI9mNyza7D8w4CVCcaOsy7sgwDzvx6ekr6cuWJqY3UGzgnQ1+4wgENup5eIhgxEYA=="], + + "cliui": ["cliui@4.1.0", "", { "dependencies": { "string-width": "^2.1.1", "strip-ansi": "^4.0.0", "wrap-ansi": "^2.0.0" } }, "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "clone-buffer": ["clone-buffer@1.0.0", "", {}, "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g=="], + + "clone-stats": ["clone-stats@1.0.0", "", {}, "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag=="], + + "cloneable-readable": ["cloneable-readable@1.1.3", "", { "dependencies": { "inherits": "^2.0.1", "process-nextick-args": "^2.0.0", "readable-stream": "^2.3.5" } }, "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ=="], + + "co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="], + + "code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="], + + "collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "date-format": ["date-format@0.0.2", "", {}, "sha512-M4obuJx8jU5T91lcbwi0+QPNVaWOY1DQYz5xUuKYWO93osVzB2ZPqyDUc5T+mDjbA1X8VOb4JDZ+8r2MrSOp7Q=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], + + "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + + "deep-equal": ["deep-equal@2.2.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", "es-get-iterator": "^1.1.3", "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "isarray": "^2.0.5", "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", "which-typed-array": "^1.1.13" } }, "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "dockerfilelint": ["dockerfilelint@1.8.0", "", { "dependencies": { "chalk": "^2.4.2", "cliui": "^4.1.0", "js-yaml": "^3.6.0", "lodash": "^4.3.0", "yargs": "^13.2.1" }, "bin": { "dockerfilelint": "bin/dockerfilelint" } }, "sha512-j0tipeP1kpTWfx1XV6QVrrJTtGiP/46+3NT5JuaqXUnYrNlusgvrSP4/ACkqQdglJfmeedIU7c2wztmxEV+JQA=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "eclint": ["eclint@2.8.1", "", { "dependencies": { "editorconfig": "^0.15.2", "file-type": "^10.1.0", "gulp-exclude-gitignore": "^1.2.0", "gulp-filter": "^5.1.0", "gulp-reporter": "^2.9.0", "gulp-tap": "^1.0.1", "linez": "^4.1.4", "lodash": "^4.17.11", "minimatch": "^3.0.4", "os-locale": "^3.0.1", "plugin-error": "^1.0.1", "through2": "^2.0.3", "vinyl": "^2.2.0", "vinyl-fs": "^3.0.3", "yargs": "^12.0.2" }, "bin": { "eclint": "bin/eclint.js" } }, "sha512-0u1UubFXSOgZgXNhuPeliYyTFmjWStVph8JR6uD6NDuxl3xI5VSCsA1KX6/BSYtM9v4wQMifGoNFfN5VlRn4LQ=="], + + "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="], + + "editorconfig": ["editorconfig@0.15.3", "", { "dependencies": { "commander": "^2.19.0", "lru-cache": "^4.1.5", "semver": "^5.6.0", "sigmund": "^1.0.1" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.214", "", {}, "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q=="], + + "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], + + "emoji-regex": ["emoji-regex@7.0.3", "", {}, "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="], + + "emphasize": ["emphasize@2.1.0", "", { "dependencies": { "chalk": "^2.4.0", "highlight.js": "~9.12.0", "lowlight": "~1.9.0" } }, "sha512-wRlO0Qulw2jieQynsS3STzTabIhHCyjTjZraSkchOiT8rdvWZlahJAJ69HRxwGkv2NThmci2MSnDfJ60jB39tw=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-get-iterator": ["es-get-iterator@1.1.3", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "has-symbols": "^1.0.3", "is-arguments": "^1.1.1", "is-map": "^2.0.2", "is-set": "^2.0.2", "is-string": "^1.0.7", "isarray": "^2.0.5", "stop-iteration-iterator": "^1.0.0" } }, "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="], + + "eslint-define-config": ["eslint-define-config@2.1.0", "", {}, "sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "execa": ["execa@5.1.1", "", { "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" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "exit-x": ["exit-x@0.2.2", "", {}, "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ=="], + + "expect": ["expect@30.1.2", "", { "dependencies": { "@jest/expect-utils": "30.1.2", "@jest/get-type": "30.1.0", "jest-matcher-utils": "30.1.2", "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" } }, "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], + + "fancy-log": ["fancy-log@1.3.3", "", { "dependencies": { "ansi-gray": "^0.1.1", "color-support": "^1.1.3", "parse-node-version": "^1.0.0", "time-stamp": "^1.0.0" } }, "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="], + + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "file-type": ["file-type@10.11.0", "", {}, "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "flush-write-stream": ["flush-write-stream@1.1.1", "", { "dependencies": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" } }, "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w=="], + + "follow-redirects": ["follow-redirects@1.5.10", "", { "dependencies": { "debug": "=3.1.0" } }, "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "fs-mkdirp-stream": ["fs-mkdirp-stream@1.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11", "through2": "^2.0.3" } }, "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.1", "", {}, "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "glob-stream": ["glob-stream@6.1.0", "", { "dependencies": { "extend": "^3.0.0", "glob": "^7.1.1", "glob-parent": "^3.1.0", "is-negated-glob": "^1.0.0", "ordered-read-streams": "^1.0.0", "pumpify": "^1.3.5", "readable-stream": "^2.1.5", "remove-trailing-separator": "^1.0.1", "to-absolute-glob": "^2.0.0", "unique-stream": "^2.0.2" } }, "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "gulp-exclude-gitignore": ["gulp-exclude-gitignore@1.2.0", "", { "dependencies": { "gulp-ignore": "^2.0.2" } }, "sha512-J3LCmz9C1UU1pxf5Npx6SNc5o9YQptyc9IHaqLiBlihZmg44jaaTplWUZ0JPQkMdOTae0YgEDvT9TKlUWDSMUA=="], + + "gulp-filter": ["gulp-filter@5.1.0", "", { "dependencies": { "multimatch": "^2.0.0", "plugin-error": "^0.1.2", "streamfilter": "^1.0.5" } }, "sha512-ZERu1ipbPmjrNQ2dQD6lL4BjrJQG66P/c5XiyMMBqV+tUAJ+fLOyYIL/qnXd2pHmw/G/r7CLQb9ttANvQWbpfQ=="], + + "gulp-ignore": ["gulp-ignore@2.0.2", "", { "dependencies": { "gulp-match": "^1.0.3", "through2": "^2.0.1" } }, "sha512-KGtd/qgp0FLDlei986/aZ5xSyw1cqJ2BsiaWht0L0PzaQXxYKRCMkFcDPQ8fQx6JVA6Gx9OefmBFzxTtop5hMw=="], + + "gulp-match": ["gulp-match@1.1.0", "", { "dependencies": { "minimatch": "^3.0.3" } }, "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ=="], + + "gulp-reporter": ["gulp-reporter@2.10.0", "", { "dependencies": { "ansi-escapes": "^3.1.0", "axios": "^0.18.0", "buffered-spawn": "^3.3.2", "bufferstreams": "^2.0.1", "chalk": "^2.4.1", "checkstyle-formatter": "^1.1.0", "ci-info": "^2.0.0", "cli-truncate": "^1.1.0", "emphasize": "^2.0.0", "fancy-log": "^1.3.3", "fs-extra": "^7.0.1", "in-gfw": "^1.2.0", "is-windows": "^1.0.2", "js-yaml": "^3.12.0", "junit-report-builder": "^1.3.1", "lodash.get": "^4.4.2", "os-locale": "^3.0.1", "plugin-error": "^1.0.1", "string-width": "^3.0.0", "term-size": "^1.2.0", "through2": "^3.0.0", "to-time": "^1.0.2" } }, "sha512-HeruxN7TL/enOB+pJfFmeekVsXsZzQvVGpL7vOLdUe7y7VdqHUvMQRRW5qMIvVSKqRs3EtQiR/kURu3WWfXq6w=="], + + "gulp-tap": ["gulp-tap@1.0.1", "", { "dependencies": { "through2": "^2.0.3" } }, "sha512-VpCARRSyr+WP16JGnoIg98/AcmyQjOwCpQgYoE35CWTdEMSbpgtAIK2fndqv2yY7aXstW27v3ZNBs0Ltb0Zkbg=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "highlight.js": ["highlight.js@9.12.0", "", {}, "sha512-qNnYpBDO/FQwYVur1+sQBQw7v0cxso1nOYLklqWh6af8ROwwTVoII5+kf/BVa8354WL4ad6rURHYGUXCbD9mMg=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "in-gfw": ["in-gfw@1.2.0", "", { "dependencies": { "glob": "^7.1.2", "is-wsl": "^1.1.0", "mem": "^3.0.1" } }, "sha512-LgSoQXzuSS/x/nh0eIggq7PsI7gs/sQdXNEolRmHaFUj6YMFmPO1kxQ7XpcT3nPpC3DMwYiJmgnluqJmFXYiMg=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "invert-kv": ["invert-kv@2.0.0", "", {}, "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA=="], + + "is-absolute": ["is-absolute@1.0.0", "", { "dependencies": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" } }, "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + + "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negated-glob": ["is-negated-glob@1.0.0", "", {}, "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-relative": ["is-relative@1.0.0", "", { "dependencies": { "is-unc-path": "^1.0.0" } }, "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-unc-path": ["is-unc-path@1.0.0", "", { "dependencies": { "unc-path-regex": "^0.1.2" } }, "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ=="], + + "is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="], + + "is-valid-glob": ["is-valid-glob@1.0.0", "", {}, "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@6.0.3", "", { "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" } }, "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "istextorbinary": ["istextorbinary@9.5.0", "", { "dependencies": { "binaryextensions": "^6.11.0", "editions": "^6.21.0", "textextensions": "^6.11.0" } }, "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jest": ["jest@30.1.3", "", { "dependencies": { "@jest/core": "30.1.3", "@jest/types": "30.0.5", "import-local": "^3.2.0", "jest-cli": "30.1.3" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": "./bin/jest.js" }, "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ=="], + + "jest-changed-files": ["jest-changed-files@30.0.5", "", { "dependencies": { "execa": "^5.1.1", "jest-util": "30.0.5", "p-limit": "^3.1.0" } }, "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A=="], + + "jest-circus": ["jest-circus@30.1.3", "", { "dependencies": { "@jest/environment": "30.1.2", "@jest/expect": "30.1.2", "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", "jest-each": "30.1.0", "jest-matcher-utils": "30.1.2", "jest-message-util": "30.1.0", "jest-runtime": "30.1.3", "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "p-limit": "^3.1.0", "pretty-format": "30.0.5", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA=="], + + "jest-cli": ["jest-cli@30.1.3", "", { "dependencies": { "@jest/core": "30.1.3", "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", "jest-config": "30.1.3", "jest-util": "30.0.5", "jest-validate": "30.1.0", "yargs": "^17.7.2" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "./bin/jest.js" } }, "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ=="], + + "jest-config": ["jest-config@30.1.3", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", "@jest/test-sequencer": "30.1.3", "@jest/types": "30.0.5", "babel-jest": "30.1.2", "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.1.3", "jest-docblock": "30.0.1", "jest-environment-node": "30.1.2", "jest-regex-util": "30.0.1", "jest-resolve": "30.1.3", "jest-runner": "30.1.3", "jest-util": "30.0.5", "jest-validate": "30.1.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "esbuild-register": ">=3.4.0", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "esbuild-register", "ts-node"] }, "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw=="], + + "jest-diff": ["jest-diff@30.1.2", "", { "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.0.5" } }, "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ=="], + + "jest-docblock": ["jest-docblock@30.0.1", "", { "dependencies": { "detect-newline": "^3.1.0" } }, "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA=="], + + "jest-each": ["jest-each@30.1.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.0.5", "chalk": "^4.1.2", "jest-util": "30.0.5", "pretty-format": "30.0.5" } }, "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ=="], + + "jest-environment-jsdom": ["jest-environment-jsdom@30.1.2", "", { "dependencies": { "@jest/environment": "30.1.2", "@jest/environment-jsdom-abstract": "30.1.2", "@types/jsdom": "^21.1.7", "@types/node": "*", "jsdom": "^26.1.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-LXsfAh5+mDTuXDONGl1ZLYxtJEaS06GOoxJb2arcJTjIfh1adYg8zLD8f6P0df8VmjvCaMrLmc1PgHUI/YUTbg=="], + + "jest-environment-node": ["jest-environment-node@30.1.2", "", { "dependencies": { "@jest/environment": "30.1.2", "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5", "jest-util": "30.0.5", "jest-validate": "30.1.0" } }, "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA=="], + + "jest-haste-map": ["jest-haste-map@30.1.0", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "jest-worker": "30.1.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.3" } }, "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg=="], + + "jest-leak-detector": ["jest-leak-detector@30.1.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "pretty-format": "30.0.5" } }, "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g=="], + + "jest-matcher-utils": ["jest-matcher-utils@30.1.2", "", { "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "jest-diff": "30.1.2", "pretty-format": "30.0.5" } }, "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ=="], + + "jest-message-util": ["jest-message-util@30.1.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg=="], + + "jest-mock": ["jest-mock@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "jest-util": "30.0.5" } }, "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ=="], + + "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" }, "optionalPeers": ["jest-resolve"] }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], + + "jest-regex-util": ["jest-regex-util@30.0.1", "", {}, "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA=="], + + "jest-resolve": ["jest-resolve@30.1.3", "", { "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "jest-haste-map": "30.1.0", "jest-pnp-resolver": "^1.2.3", "jest-util": "30.0.5", "jest-validate": "30.1.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" } }, "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw=="], + + "jest-resolve-dependencies": ["jest-resolve-dependencies@30.1.3", "", { "dependencies": { "jest-regex-util": "30.0.1", "jest-snapshot": "30.1.2" } }, "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg=="], + + "jest-runner": ["jest-runner@30.1.3", "", { "dependencies": { "@jest/console": "30.1.2", "@jest/environment": "30.1.2", "@jest/test-result": "30.1.3", "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.0.1", "jest-environment-node": "30.1.2", "jest-haste-map": "30.1.0", "jest-leak-detector": "30.1.0", "jest-message-util": "30.1.0", "jest-resolve": "30.1.3", "jest-runtime": "30.1.3", "jest-util": "30.0.5", "jest-watcher": "30.1.3", "jest-worker": "30.1.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ=="], + + "jest-runtime": ["jest-runtime@30.1.3", "", { "dependencies": { "@jest/environment": "30.1.2", "@jest/fake-timers": "30.1.2", "@jest/globals": "30.1.2", "@jest/source-map": "30.0.1", "@jest/test-result": "30.1.3", "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@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.1.0", "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-regex-util": "30.0.1", "jest-resolve": "30.1.3", "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA=="], + + "jest-snapshot": ["jest-snapshot@30.1.2", "", { "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.1.2", "@jest/get-type": "30.1.0", "@jest/snapshot-utils": "30.1.2", "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", "expect": "30.1.2", "graceful-fs": "^4.2.11", "jest-diff": "30.1.2", "jest-matcher-utils": "30.1.2", "jest-message-util": "30.1.0", "jest-util": "30.0.5", "pretty-format": "30.0.5", "semver": "^7.7.2", "synckit": "^0.11.8" } }, "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jest-validate": ["jest-validate@30.1.0", "", { "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.0.5", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", "pretty-format": "30.0.5" } }, "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA=="], + + "jest-watcher": ["jest-watcher@30.1.3", "", { "dependencies": { "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", "jest-util": "30.0.5", "string-length": "^4.0.2" } }, "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ=="], + + "jest-worker": ["jest-worker@30.1.0", "", { "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" } }, "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "junit-report-builder": ["junit-report-builder@1.3.3", "", { "dependencies": { "date-format": "0.0.2", "lodash": "^4.17.15", "mkdirp": "^0.5.0", "xmlbuilder": "^10.0.0" } }, "sha512-75bwaXjP/3ogyzOSkkcshXGG7z74edkJjgTZlJGAyzxlOHaguexM3VLG6JyD9ZBF8mlpgsUPB1sIWU4LISgeJw=="], + + "katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kind-of": ["kind-of@1.1.0", "", {}, "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g=="], + + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + + "lcid": ["lcid@2.0.0", "", { "dependencies": { "invert-kv": "^2.0.0" } }, "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA=="], + + "lead": ["lead@1.0.0", "", { "dependencies": { "flush-write-stream": "^1.0.2" } }, "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "linez": ["linez@4.1.4", "", { "dependencies": { "buffer-equals": "^1.0.4", "iconv-lite": "^0.4.15" } }, "sha512-TsqcAfotPMB9xodBIklBaJz3sRIXtkca8Kv/MO8nzAufsitCKRoYWU5MZccdCVYB81tGexYHRsrSIEiJsQhpVQ=="], + + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + + "lint-staged": ["lint-staged@16.1.6", "", { "dependencies": { "chalk": "^5.6.0", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^9.0.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow=="], + + "listr2": ["listr2@9.0.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "lowlight": ["lowlight@1.9.2", "", { "dependencies": { "fault": "^1.0.2", "highlight.js": "~9.12.0" } }, "sha512-Ek18ElVCf/wF/jEm1b92gTnigh94CtBNWiZ2ad+vTgW7cTmQxUY3I98BjHK68gZAJEWmybGBZgx9qv3QxLQB/Q=="], + + "lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + + "map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="], + + "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], + + "markdownlint": ["markdownlint@0.38.0", "", { "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2" } }, "sha512-xaSxkaU7wY/0852zGApM8LdlIfGCW8ETZ0Rr62IQtAnUMlMuifsg09vWJcNYeL4f0anvr8Vo4ZQar8jGpV0btQ=="], + + "markdownlint-cli2": ["markdownlint-cli2@0.18.1", "", { "dependencies": { "globby": "14.1.0", "js-yaml": "4.1.0", "jsonc-parser": "3.3.1", "markdown-it": "14.1.0", "markdownlint": "0.38.0", "markdownlint-cli2-formatter-default": "0.0.5", "micromatch": "4.0.8" }, "bin": { "markdownlint-cli2": "markdownlint-cli2-bin.mjs" } }, "sha512-/4Osri9QFGCZOCTkfA8qJF+XGjKYERSHkXzxSyS1hd3ZERJGjvsUao2h4wdnvpHp6Tu2Jh/bPHM0FE9JJza6ng=="], + + "markdownlint-cli2-formatter-default": ["markdownlint-cli2-formatter-default@0.0.5", "", { "peerDependencies": { "markdownlint-cli2": ">=0.0.4" } }, "sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + + "mem": ["mem@4.3.0", "", { "dependencies": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", "p-is-promise": "^2.0.0" } }, "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "multimatch": ["multimatch@2.1.0", "", { "dependencies": { "array-differ": "^1.0.0", "array-union": "^1.0.1", "arrify": "^1.0.0", "minimatch": "^3.0.0" } }, "sha512-0mzK8ymiWdehTBiJh0vClAzGyQbdtyWqzSVx//EK4N/D+599RFlGfTAsKw2zMSABtDG9C6Ul2+t8f2Lbdjf5mA=="], + + "nano-spawn": ["nano-spawn@1.0.3", "", {}, "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "node-releases": ["node-releases@2.0.20", "", {}, "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA=="], + + "normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="], + + "now-and-later": ["now-and-later@2.0.1", "", { "dependencies": { "once": "^1.3.2" } }, "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "number-is-nan": ["number-is-nan@1.0.1", "", {}, "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ=="], + + "nwsapi": ["nwsapi@2.2.22", "", {}, "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "ordered-read-streams": ["ordered-read-streams@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.1" } }, "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw=="], + + "os-locale": ["os-locale@3.1.0", "", { "dependencies": { "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } }, "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q=="], + + "p-defer": ["p-defer@1.0.0", "", {}, "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="], + + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], + + "p-is-promise": ["p-is-promise@2.1.0", "", {}, "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-json": ["parse-json@5.2.0", "", { "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" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-node-version": ["parse-node-version@1.0.1", "", {}, "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-dirname": ["path-dirname@1.0.2", "", {}, "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "playwright": ["playwright@1.55.0", "", { "dependencies": { "playwright-core": "1.55.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA=="], + + "playwright-core": ["playwright-core@1.55.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg=="], + + "plugin-error": ["plugin-error@1.0.1", "", { "dependencies": { "ansi-colors": "^1.0.1", "arr-diff": "^4.0.0", "arr-union": "^3.1.0", "extend-shallow": "^3.0.2" } }, "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "pseudomap": ["pseudomap@1.0.2", "", {}, "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="], + + "pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="], + + "pumpify": ["pumpify@1.5.1", "", { "dependencies": { "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" } }, "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "rc-config-loader": ["rc-config-loader@4.1.3", "", { "dependencies": { "debug": "^4.3.4", "js-yaml": "^4.1.0", "json5": "^2.2.2", "require-from-string": "^2.0.2" } }, "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w=="], + + "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], + + "react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "remove-bom-buffer": ["remove-bom-buffer@3.0.0", "", { "dependencies": { "is-buffer": "^1.1.5", "is-utf8": "^0.2.1" } }, "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ=="], + + "remove-bom-stream": ["remove-bom-stream@1.2.0", "", { "dependencies": { "remove-bom-buffer": "^3.0.0", "safe-buffer": "^5.1.0", "through2": "^2.0.3" } }, "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA=="], + + "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="], + + "replace-ext": ["replace-ext@1.0.1", "", {}, "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-options": ["resolve-options@1.1.0", "", { "dependencies": { "value-or-function": "^3.0.0" } }, "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shelljs": ["shelljs@0.10.0", "", { "dependencies": { "execa": "^5.1.1", "fast-glob": "^3.3.2" } }, "sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw=="], + + "side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "sigmund": ["sigmund@1.0.1", "", {}, "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + + "streamfilter": ["streamfilter@1.0.7", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + + "string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], + + "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + + "term-size": ["term-size@1.2.0", "", { "dependencies": { "execa": "^0.7.0" } }, "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ=="], + + "terminal-link": ["terminal-link@4.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^3.2.0" } }, "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], + + "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], + + "through2-filter": ["through2-filter@3.1.0", "", { "dependencies": { "through2": "^4.0.2" } }, "sha512-VhZsTsfrIJjyUi6GeecnwcOJlmoqgIdGFDjqnV5ape+F1DN8GejfPO66XyIhoinxmxGImiUTrq9RwpTN5yszGA=="], + + "time-stamp": ["time-stamp@1.1.0", "", {}, "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], + + "to-absolute-glob": ["to-absolute-glob@2.0.2", "", { "dependencies": { "is-absolute": "^1.0.0", "is-negated-glob": "^1.0.0" } }, "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "to-through": ["to-through@2.0.0", "", { "dependencies": { "through2": "^2.0.3" } }, "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q=="], + + "to-time": ["to-time@1.0.2", "", { "dependencies": { "bignumber.js": "^2.4.0" } }, "sha512-+wqaiQvnido2DI1bpiQ/Zv1LiOE9Fd0v35ySnNeqFmKNYJTJY/+ENI+3sHXCMzbAAOR/43aNyLM0XTpi0/zSQg=="], + + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "ts-jest": ["ts-jest@29.4.1", "", { "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "unc-path-regex": ["unc-path-regex@0.1.2", "", {}, "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "unique-stream": ["unique-stream@2.3.1", "", { "dependencies": { "json-stable-stringify-without-jsonify": "^1.0.1", "through2-filter": "^3.0.0" } }, "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@12.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA=="], + + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + + "value-or-function": ["value-or-function@3.0.0", "", {}, "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg=="], + + "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], + + "vinyl": ["vinyl@2.2.1", "", { "dependencies": { "clone": "^2.1.1", "clone-buffer": "^1.0.0", "clone-stats": "^1.0.0", "cloneable-readable": "^1.0.0", "remove-trailing-separator": "^1.0.1", "replace-ext": "^1.0.0" } }, "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw=="], + + "vinyl-fs": ["vinyl-fs@3.0.3", "", { "dependencies": { "fs-mkdirp-stream": "^1.0.0", "glob-stream": "^6.1.0", "graceful-fs": "^4.0.0", "is-valid-glob": "^1.0.0", "lazystream": "^1.0.0", "lead": "^1.0.0", "object.assign": "^4.0.4", "pumpify": "^1.3.5", "readable-stream": "^2.3.3", "remove-bom-buffer": "^3.0.0", "remove-bom-stream": "^1.2.0", "resolve-options": "^1.1.0", "through2": "^2.0.0", "to-through": "^2.0.0", "value-or-function": "^3.0.0", "vinyl": "^2.0.0", "vinyl-sourcemap": "^1.1.0" } }, "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng=="], + + "vinyl-sourcemap": ["vinyl-sourcemap@1.1.0", "", { "dependencies": { "append-buffer": "^1.0.2", "convert-source-map": "^1.5.0", "graceful-fs": "^4.1.6", "normalize-path": "^2.1.1", "now-and-later": "^2.0.0", "remove-bom-buffer": "^3.0.0", "vinyl": "^2.0.0" } }, "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA=="], + + "vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "wrap-ansi": ["wrap-ansi@2.1.0", "", { "dependencies": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1" } }, "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "xml-escape": ["xml-escape@1.1.0", "", {}, "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "yallist": ["yallist@2.1.2", "", {}, "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="], + + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + + "yargs": ["yargs@13.3.2", "", { "dependencies": { "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^13.1.2" } }, "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@istanbuljs/load-nyc-config/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/console/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "@jest/core/@jest/transform": ["@jest/transform@30.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@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.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA=="], + + "@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "@jest/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/core/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "@jest/core/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "@jest/environment-jsdom-abstract/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "@jest/fake-timers/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "@jest/reporters/@jest/transform": ["@jest/transform@30.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@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.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA=="], + + "@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/reporters/glob": ["glob@10.4.5", "", { "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" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@jest/reporters/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "@jest/snapshot-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/transform/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "@jest/transform/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/transform/jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], + + "@jest/transform/jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], + + "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@secretlint/config-loader/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "@secretlint/formatter/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], + + "@testing-library/dom/aria-query": ["aria-query@5.1.3", "", { "dependencies": { "deep-equal": "^2.0.5" } }, "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ=="], + + "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "@testing-library/dom/pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "@textlint/linter-formatter/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], + + "@textlint/linter-formatter/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "anymatch/normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "babel-jest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "buffered-spawn/cross-spawn": ["cross-spawn@4.0.2", "", { "dependencies": { "lru-cache": "^4.0.1", "which": "^1.2.9" } }, "sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA=="], + + "chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "cli-truncate/slice-ansi": ["slice-ansi@1.0.0", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0" } }, "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg=="], + + "cliui/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "deep-equal/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "eclint/yargs": ["yargs@12.0.5", "", { "dependencies": { "cliui": "^4.0.0", "decamelize": "^1.2.0", "find-up": "^3.0.0", "get-caller-file": "^1.0.1", "os-locale": "^3.0.0", "require-directory": "^2.1.1", "require-main-filename": "^1.0.1", "set-blocking": "^2.0.0", "string-width": "^2.0.0", "which-module": "^2.0.0", "y18n": "^3.2.1 || ^4.0.0", "yargs-parser": "^11.1.1" } }, "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw=="], + + "editorconfig/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "editorconfig/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "expect/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "follow-redirects/debug": ["debug@3.1.0", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g=="], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "glob-stream/glob-parent": ["glob-parent@3.1.0", "", { "dependencies": { "is-glob": "^3.1.0", "path-dirname": "^1.0.0" } }, "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA=="], + + "globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + + "gulp-filter/plugin-error": ["plugin-error@0.1.2", "", { "dependencies": { "ansi-cyan": "^0.1.1", "ansi-red": "^0.1.1", "arr-diff": "^1.0.1", "arr-union": "^2.0.1", "extend-shallow": "^1.1.2" } }, "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw=="], + + "gulp-reporter/string-width": ["string-width@3.1.0", "", { "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w=="], + + "gulp-reporter/through2": ["through2@3.0.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "2 || 3" } }, "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ=="], + + "in-gfw/mem": ["mem@3.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0", "p-is-promise": "^1.1.0" } }, "sha512-QKs47bslvOE0NbXOqG6lMxn6Bk0Iuw0vfrIeLykmQle2LkCw1p48dZDdzE+D88b/xqRJcZGcMNeDvSVma+NuIQ=="], + + "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-changed-files/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-circus/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-circus/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-cli/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-cli/yargs": ["yargs@17.7.2", "", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "jest-config/babel-jest": ["babel-jest@30.1.2", "", { "dependencies": { "@jest/transform": "30.1.2", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.0", "babel-preset-jest": "30.0.1", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.11.0" } }, "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g=="], + + "jest-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-config/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-config/glob": ["glob@10.4.5", "", { "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" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "jest-config/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-each/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-each/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-environment-node/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-haste-map/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-mock/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-resolve/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-resolve/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-runner/@jest/transform": ["@jest/transform@30.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@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.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA=="], + + "jest-runner/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-runner/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-runtime/@jest/transform": ["@jest/transform@30.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@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.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA=="], + + "jest-runtime/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-runtime/glob": ["glob@10.4.5", "", { "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" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "jest-runtime/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-snapshot/@jest/transform": ["@jest/transform@30.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@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.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA=="], + + "jest-snapshot/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-snapshot/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "jest-validate/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-watcher/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "jest-watcher/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-watcher/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-worker/jest-util": ["jest-util@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "lint-staged/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], + + "listr2/cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + + "listr2/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "log-update/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "markdown-it/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "markdownlint-cli2/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "os-locale/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "rc-config-loader/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "remove-bom-buffer/is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "string-length/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "string-width/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "supports-hyperlinks/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "table/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "term-size/execa": ["execa@0.7.0", "", { "dependencies": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw=="], + + "terminal-link/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], + + "through2-filter/through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "vinyl-sourcemap/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "wrap-ansi/string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/cliui": ["cliui@5.0.0", "", { "dependencies": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", "wrap-ansi": "^5.1.0" } }, "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA=="], + + "yargs/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "yargs/string-width": ["string-width@3.1.0", "", { "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w=="], + + "yargs/yargs-parser": ["yargs-parser@13.1.2", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@jest/console/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/console/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "@jest/core/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "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" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "@jest/core/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "@jest/core/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/environment-jsdom-abstract/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/environment-jsdom-abstract/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "@jest/fake-timers/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/fake-timers/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "@jest/reporters/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "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" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "@jest/reporters/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "@jest/reporters/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/reporters/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/reporters/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@jest/reporters/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "@jest/snapshot-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/snapshot-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/transform/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/transform/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/transform/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/transform/jest-haste-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@secretlint/config-loader/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@testing-library/dom/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@testing-library/dom/pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "@textlint/linter-formatter/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@textlint/linter-formatter/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@textlint/linter-formatter/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@textlint/linter-formatter/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "babel-jest/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "babel-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "buffered-spawn/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "eclint/yargs/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "eclint/yargs/get-caller-file": ["get-caller-file@1.0.3", "", {}, "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="], + + "eclint/yargs/require-main-filename": ["require-main-filename@1.0.1", "", {}, "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug=="], + + "eclint/yargs/yargs-parser": ["yargs-parser@11.1.1", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ=="], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "expect/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "expect/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "follow-redirects/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "glob-stream/glob-parent/is-glob": ["is-glob@3.1.0", "", { "dependencies": { "is-extglob": "^2.1.0" } }, "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw=="], + + "gulp-filter/plugin-error/arr-diff": ["arr-diff@1.1.0", "", { "dependencies": { "arr-flatten": "^1.0.1", "array-slice": "^0.2.3" } }, "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q=="], + + "gulp-filter/plugin-error/arr-union": ["arr-union@2.1.0", "", {}, "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA=="], + + "gulp-filter/plugin-error/extend-shallow": ["extend-shallow@1.1.4", "", { "dependencies": { "kind-of": "^1.1.0" } }, "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw=="], + + "gulp-reporter/string-width/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + + "in-gfw/mem/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "in-gfw/mem/p-is-promise": ["p-is-promise@1.1.0", "", {}, "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg=="], + + "istanbul-lib-report/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-changed-files/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-changed-files/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-circus/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-circus/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-circus/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-cli/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-cli/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-cli/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-cli/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "jest-cli/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "jest-cli/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "jest-config/babel-jest/@jest/transform": ["@jest/transform@30.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.0.5", "@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.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA=="], + + "jest-config/babel-jest/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "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" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "jest-config/babel-jest/babel-preset-jest": ["babel-preset-jest@30.0.1", "", { "dependencies": { "babel-plugin-jest-hoist": "30.0.1", "babel-preset-current-node-syntax": "^1.1.0" }, "peerDependencies": { "@babel/core": "^7.11.0" } }, "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw=="], + + "jest-config/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-each/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-each/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-each/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-environment-node/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-environment-node/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-haste-map/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-haste-map/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-mock/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-mock/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-resolve/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-resolve/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-resolve/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-runner/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "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" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "jest-runner/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "jest-runner/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-runner/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-runner/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-runtime/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "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" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "jest-runtime/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "jest-runtime/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-runtime/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-runtime/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "jest-runtime/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-snapshot/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "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" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "jest-snapshot/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "jest-snapshot/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-snapshot/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-snapshot/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-validate/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-validate/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-watcher/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "jest-watcher/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-watcher/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-watcher/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-worker/jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-worker/jest-util/ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + + "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "listr2/cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "listr2/cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "listr2/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "listr2/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "markdownlint-cli2/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "os-locale/execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "os-locale/execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], + + "os-locale/execa/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "os-locale/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "rc-config-loader/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "string-length/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "table/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "table/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "term-size/execa/cross-spawn": ["cross-spawn@5.1.0", "", { "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A=="], + + "term-size/execa/get-stream": ["get-stream@3.0.0", "", {}, "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ=="], + + "term-size/execa/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "term-size/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + + "through2-filter/through2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "yargs/cliui/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + + "yargs/cliui/wrap-ansi": ["wrap-ansi@5.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" } }, "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q=="], + + "yargs/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@jest/console/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/console/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/core/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "@jest/core/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/core/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/environment-jsdom-abstract/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/environment-jsdom-abstract/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/fake-timers/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@jest/fake-timers/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/reporters/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "@jest/reporters/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/reporters/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/reporters/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@jest/snapshot-utils/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/snapshot-utils/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/transform/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@jest/transform/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/transform/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/transform/jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "@jest/types/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/types/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@testing-library/dom/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@testing-library/dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@textlint/linter-formatter/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@textlint/linter-formatter/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "babel-jest/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "babel-jest/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "eclint/yargs/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "eslint/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "expect/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "expect/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "gulp-reporter/string-width/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + + "jest-changed-files/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-changed-files/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-circus/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-circus/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-cli/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-cli/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "jest-cli/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "jest-cli/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "jest-cli/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "jest-cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "jest-config/babel-jest/@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "jest-config/babel-jest/babel-preset-jest/babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@30.0.1", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "@types/babel__core": "^7.20.5" } }, "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ=="], + + "jest-config/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-config/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-config/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "jest-diff/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-diff/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-each/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-each/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-environment-node/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-environment-node/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-haste-map/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-haste-map/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-matcher-utils/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-matcher-utils/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-message-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-message-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-mock/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-mock/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-resolve/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-resolve/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-runner/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "jest-runner/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-runner/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-runtime/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "jest-runtime/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-runtime/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-runtime/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "jest-snapshot/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "jest-snapshot/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-snapshot/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-util/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-validate/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-validate/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-watcher/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-watcher/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-worker/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-worker/jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "listr2/cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "listr2/cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "listr2/cli-truncate/string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + + "listr2/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + + "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + + "os-locale/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "os-locale/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "os-locale/execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "os-locale/execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "os-locale/execa/get-stream/pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "os-locale/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "term-size/execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "term-size/execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "term-size/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + + "yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "yargs/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "yargs/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@jest/console/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@jest/core/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@jest/environment-jsdom-abstract/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/environment-jsdom-abstract/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/fake-timers/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@jest/fake-timers/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/reporters/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@jest/snapshot-utils/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@jest/transform/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@jest/transform/jest-haste-map/jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "@jest/types/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@testing-library/dom/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@textlint/linter-formatter/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "babel-jest/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "eclint/yargs/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "eclint/yargs/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "expect/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "expect/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-changed-files/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-changed-files/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-circus/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-cli/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-cli/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "jest-cli/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "jest-cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "jest-config/babel-jest/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "jest-config/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-diff/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-each/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-environment-node/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-environment-node/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-haste-map/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-haste-map/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-matcher-utils/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-message-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-mock/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-mock/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "jest-resolve/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-runner/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-runtime/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-snapshot/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-validate/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-watcher/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-worker/jest-util/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-worker/jest-util/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "os-locale/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "term-size/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@jest/environment-jsdom-abstract/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "@jest/fake-timers/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "eclint/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "expect/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-changed-files/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-cli/yargs/cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "jest-environment-node/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-haste-map/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-mock/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-worker/jest-util/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "jest-cli/yargs/cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + } +} diff --git a/components/README.md b/components/README.md new file mode 100644 index 0000000..6feebe7 --- /dev/null +++ b/components/README.md @@ -0,0 +1,98 @@ +# 🧩 Component Architecture + +## Component Organization + +The components are organized by feature and responsibility for better maintainability: + +``` +components/ +├── medication/ # Medication-related components +│ ├── AddMedicationModal.tsx +│ ├── EditMedicationModal.tsx +│ ├── ManageMedicationsModal.tsx +│ ├── DoseCard.tsx +│ └── index.ts +├── auth/ # Authentication components +│ ├── AuthPage.tsx +│ ├── AvatarDropdown.tsx +│ ├── ChangePasswordModal.tsx +│ └── index.ts +├── admin/ # Admin interface components +│ ├── AdminInterface.tsx +│ └── index.ts +├── modals/ # Generic modal components +│ ├── AccountModal.tsx +│ ├── AddReminderModal.tsx +│ ├── EditReminderModal.tsx +│ ├── HistoryModal.tsx +│ ├── ManageRemindersModal.tsx +│ ├── OnboardingModal.tsx +│ ├── StatsModal.tsx +│ └── index.ts +├── ui/ # Reusable UI components +│ ├── BarChart.tsx +│ ├── ReminderCard.tsx +│ ├── ThemeSwitcher.tsx +│ └── index.ts +└── icons/ # Icon components + └── Icons.tsx +``` + +## Import Structure + +### Feature-Based Imports + +```tsx +// Medication components +import { AddMedicationModal, DoseCard } from './components/medication'; + +// Authentication components +import { AuthPage, AvatarDropdown } from './components/auth'; + +// Modal components +import { AccountModal, StatsModal } from './components/modals'; + +// UI components +import { BarChart, ThemeSwitcher } from './components/ui'; +``` + +## Component Categories + +### 🏥 **Medication Components** + +- **Purpose**: Medication management and dose tracking +- **Components**: AddMedicationModal, EditMedicationModal, ManageMedicationsModal, DoseCard +- **Responsibility**: CRUD operations for medications and dose status management + +### 🔐 **Authentication Components** + +- **Purpose**: User authentication and profile management +- **Components**: AuthPage, AvatarDropdown, ChangePasswordModal +- **Responsibility**: Login/register, user menus, credential management + +### 👑 **Admin Components** + +- **Purpose**: Administrative functionality +- **Components**: AdminInterface +- **Responsibility**: User management, system administration + +### 🎛️ **Modal Components** + +- **Purpose**: Overlay interfaces for specific actions +- **Components**: AccountModal, AddReminderModal, EditReminderModal, HistoryModal, ManageRemindersModal, OnboardingModal, StatsModal +- **Responsibility**: Focused user interactions in modal format + +### 🎨 **UI Components** + +- **Purpose**: Reusable interface elements +- **Components**: BarChart, ReminderCard, ThemeSwitcher +- **Responsibility**: Visual presentation and data display + +## Benefits of This Organization + +✅ **Feature Cohesion** - Related components grouped together +✅ **Easy Navigation** - Clear folder structure +✅ **Reduced Import Complexity** - Index files for clean imports +✅ **Better Maintainability** - Logical separation of concerns +✅ **Scalability** - Easy to add new components in appropriate categories +✅ **Testing** - Each feature can be tested independently diff --git a/components/admin/AdminInterface.tsx b/components/admin/AdminInterface.tsx new file mode 100644 index 0000000..4b4bdf9 --- /dev/null +++ b/components/admin/AdminInterface.tsx @@ -0,0 +1,361 @@ +import React, { useState, useEffect } from 'react'; +import { User, UserRole } from '../../types'; +import { AccountStatus } from '../../services/auth/auth.constants'; +import { dbService } from '../../services/couchdb.factory'; +import { useUser } from '../../contexts/UserContext'; + +interface AdminInterfaceProps { + onClose: () => void; +} + +const AdminInterface: React.FC = ({ onClose }) => { + const { user: currentUser } = useUser(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [selectedUser, setSelectedUser] = useState(null); + const [newPassword, setNewPassword] = useState(''); + + useEffect(() => { + loadUsers(); + }, []); + + const loadUsers = async () => { + try { + const allUsers = await dbService.getAllUsers(); + setUsers(allUsers); + } catch (error) { + setError('Failed to load users'); + console.error('Error loading users:', error); + } finally { + setLoading(false); + } + }; + + const handleSuspendUser = async (userId: string) => { + try { + await dbService.suspendUser(userId); + await loadUsers(); + } catch (error) { + setError('Failed to suspend user'); + console.error('Error suspending user:', error); + } + }; + + const handleActivateUser = async (userId: string) => { + try { + await dbService.activateUser(userId); + await loadUsers(); + } catch (error) { + setError('Failed to activate user'); + console.error('Error activating user:', error); + } + }; + + const handleDeleteUser = async (userId: string) => { + if ( + !confirm( + 'Are you sure you want to delete this user? This action cannot be undone.' + ) + ) { + return; + } + + try { + await dbService.deleteUser(userId); + await loadUsers(); + } catch (error) { + setError('Failed to delete user'); + console.error('Error deleting user:', error); + } + }; + + const handleChangePassword = async (userId: string) => { + if (!newPassword || newPassword.length < 6) { + setError('Password must be at least 6 characters long'); + return; + } + + try { + await dbService.changeUserPassword(userId, newPassword); + setNewPassword(''); + setSelectedUser(null); + setError(''); + alert('Password changed successfully'); + } catch (error) { + setError('Failed to change password'); + console.error('Error changing password:', error); + } + }; + + const getStatusColor = (status?: AccountStatus) => { + switch (status) { + case AccountStatus.ACTIVE: + return 'text-green-600 bg-green-100'; + case AccountStatus.SUSPENDED: + return 'text-red-600 bg-red-100'; + case AccountStatus.PENDING: + return 'text-yellow-600 bg-yellow-100'; + default: + return 'text-gray-600 bg-gray-100'; + } + }; + + const getRoleColor = (role?: UserRole) => { + return role === UserRole.ADMIN + ? 'text-purple-600 bg-purple-100' + : 'text-blue-600 bg-blue-100'; + }; + + if (currentUser?.role !== UserRole.ADMIN) { + return ( +
+
+

Access Denied

+

+ You don't have permission to access the admin interface. +

+ +
+
+ ); + } + + return ( +
+
+
+
+

+ Admin Interface +

+ +
+
+ +
+ {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+

+ Loading users... +

+
+ ) : ( +
+
+

+ User Management ({users.length} users) +

+ +
+ +
+ + + + + + + + + + + + + {users.map(user => ( + + + + + + + + + ))} + +
+ User + + Email + + Status + + Role + + Created + + Actions +
+
+ {user.avatar ? ( + {user.username} + ) : ( +
+ + {user.username.charAt(0).toUpperCase()} + +
+ )} +
+
+ {user.username} +
+
+ ID: {user._id.slice(-8)} +
+
+
+
+ {user.email} + {user.emailVerified && ( + + )} + + + {user.status || 'Unknown'} + + + + {user.role || 'USER'} + + + {user.createdAt + ? new Date(user.createdAt).toLocaleDateString() + : 'Unknown'} + +
+ {user.status === AccountStatus.ACTIVE ? ( + + ) : ( + + )} + + {user.password && ( + + )} + + +
+
+
+
+ )} +
+ + {/* Password Change Modal */} + {selectedUser && ( +
+
+

+ Change Password for {selectedUser.username} +

+
+
+ + setNewPassword(e.target.value)} + className='w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white' + placeholder='Enter new password (min 6 characters)' + /> +
+
+ + +
+
+
+
+ )} +
+
+ ); +}; + +export default AdminInterface; diff --git a/components/admin/index.ts b/components/admin/index.ts new file mode 100644 index 0000000..339eaf4 --- /dev/null +++ b/components/admin/index.ts @@ -0,0 +1,2 @@ +// Admin Components +export { default as AdminInterface } from './AdminInterface'; diff --git a/components/auth/AuthPage.tsx b/components/auth/AuthPage.tsx new file mode 100644 index 0000000..9d6fa22 --- /dev/null +++ b/components/auth/AuthPage.tsx @@ -0,0 +1,316 @@ +import React, { useState, useEffect } from 'react'; +import { useUser } from '../../contexts/UserContext'; +import { authService } from '../../services/auth/auth.service'; +import { PillIcon } from '../icons/Icons'; + +const AuthPage: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isSignUp, setIsSignUp] = useState(false); + const [error, setError] = useState(''); + const { login, register, loginWithOAuth } = useUser(); + + // State for email verification result + const [verificationResult, setVerificationResult] = useState< + null | 'success' | 'error' + >(null); + + // Extract token from URL and verify email + useEffect(() => { + const path = window.location.pathname; + const params = new URLSearchParams(window.location.search); + const token = params.get('token'); + if (path === '/verify-email' && token) { + authService + .verifyEmail(token) + .then(() => setVerificationResult('success')) + .catch(() => setVerificationResult('error')); + } + }, []); + + // FIX: Made the function async and added await to handle promises from login. + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (!email.trim()) { + setError('Email cannot be empty.'); + return; + } + + if (!password.trim()) { + setError('Password cannot be empty.'); + return; + } + + // Validate email format (allow localhost for admin) + const emailRegex = /^[^\s@]+@[^\s@]+$/; // Simplified to allow any domain including localhost + if (!emailRegex.test(email)) { + setError('Please enter a valid email address.'); + return; + } + + if (isSignUp) { + // Registration + if (password.length < 6) { + setError('Password must be at least 6 characters long.'); + return; + } + + if (password !== confirmPassword) { + setError('Passwords do not match.'); + return; + } + + const success = await register(email, password); + if (success) { + setError( + 'Registration successful! Please check your email for verification (demo: verification not actually sent).' + ); + setIsSignUp(false); // Switch back to login mode + setPassword(''); + setConfirmPassword(''); + } else { + setError('Registration failed. Email may already be in use.'); + } + } else { + // Login + const success = await login(email, password); + if (!success) { + setError('Login failed. Please check your email and password.'); + } + } + }; + + const handleOAuthLogin = async (provider: 'google' | 'github') => { + setError(''); + try { + // Mock OAuth data - in a real app, this would come from the OAuth provider + const mockUserData = { + email: provider === 'google' ? 'user@gmail.com' : 'user@github.com', + username: `${provider}_user_${Date.now()}`, + avatar: `https://via.placeholder.com/150?text=${provider.toUpperCase()}`, + }; + + const success = await loginWithOAuth(provider, mockUserData); + if (!success) { + setError(`${provider} authentication failed. Please try again.`); + } + } catch (error) { + setError(`${provider} authentication failed. Please try again.`); + } + }; + + if (verificationResult) { + return ( +
+
+ {verificationResult === 'success' ? ( +

+ Email verified successfully! You can now sign in. +

+ ) : ( +

+ Email verification failed. Please try again. +

+ )} + +
+
+ ); + } + + return ( +
+
+
+
+ +
+

+ Medication Reminder +

+

+ Sign in with your email or create an account +

+
+ +
+
+ + +
+ +
+
+
+ + setEmail(e.target.value)} + required + autoFocus + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + placeholder='your@email.com' + /> +
+ +
+ + setPassword(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + placeholder={ + isSignUp + ? 'Create a password (min 6 characters)' + : 'Enter your password' + } + /> +
+ + {isSignUp && ( +
+ + setConfirmPassword(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + placeholder='Confirm your password' + /> +
+ )} +
+ + {error && ( +

+ {error} +

+ )} + + +
+ +
+
+
+
+
+ + Or create an account with + +
+
+ +
+ + + +
+
+
+
+ ); +}; + +export default AuthPage; diff --git a/components/auth/AvatarDropdown.tsx b/components/auth/AvatarDropdown.tsx new file mode 100644 index 0000000..1b959ee --- /dev/null +++ b/components/auth/AvatarDropdown.tsx @@ -0,0 +1,112 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { User, UserRole } from '../../types'; + +interface AvatarDropdownProps { + user: User; + onLogout: () => void; + onAdmin?: () => void; + onChangePassword?: () => void; +} + +const getInitials = (name: string) => { + return name ? name.charAt(0).toUpperCase() : '?'; +}; + +const AvatarDropdown: React.FC = ({ + user, + onLogout, + onAdmin, + onChangePassword, +}) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+
+

+ Signed in as +

+

+ {user.username} +

+ {user.role === UserRole.ADMIN && ( +

+ Administrator +

+ )} +
+ + {/* Password Change Option - Only for password-based accounts */} + {user.password && onChangePassword && ( + + )} + + {/* Admin Interface - Only for admins */} + {user.role === UserRole.ADMIN && onAdmin && ( + + )} + + +
+ )} +
+ ); +}; + +export default AvatarDropdown; diff --git a/components/auth/ChangePasswordModal.tsx b/components/auth/ChangePasswordModal.tsx new file mode 100644 index 0000000..68adf32 --- /dev/null +++ b/components/auth/ChangePasswordModal.tsx @@ -0,0 +1,192 @@ +import React, { useState } from 'react'; +import { authService } from '../../services/auth/auth.service'; +import { useUser } from '../../contexts/UserContext'; + +interface ChangePasswordModalProps { + onClose: () => void; + onSuccess: () => void; +} + +const ChangePasswordModal: React.FC = ({ + onClose, + onSuccess, +}) => { + const { user } = useUser(); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + // Validation + if (!currentPassword || !newPassword || !confirmPassword) { + setError('All fields are required'); + setLoading(false); + return; + } + + if (newPassword.length < 6) { + setError('New password must be at least 6 characters long'); + setLoading(false); + return; + } + + if (newPassword !== confirmPassword) { + setError('New passwords do not match'); + setLoading(false); + return; + } + + if (currentPassword === newPassword) { + setError('New password must be different from current password'); + setLoading(false); + return; + } + + try { + await authService.changePassword(user!._id, currentPassword, newPassword); + onSuccess(); + onClose(); + } catch (error: any) { + setError(error.message || 'Failed to change password'); + } finally { + setLoading(false); + } + }; + + // Don't show for OAuth users + if (!user?.password) { + return ( +
+
+

+ Password Change Not Available +

+

+ This account was created using OAuth (Google/GitHub). Password + changes are not available for OAuth accounts. +

+ +
+
+ ); + } + + return ( +
+
+
+

+ Change Password +

+ +
+ +
+
+ + setCurrentPassword(e.target.value)} + className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white' + placeholder='Enter your current password' + /> +
+ +
+ + setNewPassword(e.target.value)} + className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white' + placeholder='Enter new password (min 6 characters)' + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className='w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white' + placeholder='Confirm your new password' + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+
+ ); +}; + +export default ChangePasswordModal; diff --git a/components/auth/index.ts b/components/auth/index.ts new file mode 100644 index 0000000..081e12c --- /dev/null +++ b/components/auth/index.ts @@ -0,0 +1,4 @@ +// Authentication Components +export { default as AuthPage } from './AuthPage'; +export { default as AvatarDropdown } from './AvatarDropdown'; +export { default as ChangePasswordModal } from './ChangePasswordModal'; diff --git a/components/icons/Icons.tsx b/components/icons/Icons.tsx new file mode 100644 index 0000000..e105274 --- /dev/null +++ b/components/icons/Icons.tsx @@ -0,0 +1,546 @@ +import React from 'react'; + +export const PillIcon: React.FC> = props => ( + + + + +); + +export const ClockIcon: React.FC> = props => ( + + + + +); + +export const CheckCircleIcon: React.FC< + React.SVGProps +> = props => ( + + + + +); + +export const XCircleIcon: React.FC> = props => ( + + + + + +); + +export const PlusIcon: React.FC> = props => ( + + + + +); + +export const TrashIcon: React.FC> = props => ( + + + + + + +); + +export const EditIcon: React.FC> = props => ( + + + + +); + +export const MenuIcon: React.FC> = props => ( + + + + + +); + +export const HistoryIcon: React.FC> = props => ( + + + + + +); + +export const InfoIcon: React.FC> = props => ( + + + + + +); + +export const SunIcon: React.FC> = props => ( + + + + + + + + + + + +); + +export const SunsetIcon: React.FC> = props => ( + + + + + + + + + + +); + +export const MoonIcon: React.FC> = props => ( + + + +); + +export const DesktopIcon: React.FC> = props => ( + + + + + +); + +export const SearchIcon: React.FC> = props => ( + + + + +); + +export const CapsuleIcon: React.FC> = props => ( + + + + + + +); + +export const SyringeIcon: React.FC> = props => ( + + + + + + + + +); + +export const BottleIcon: React.FC> = props => ( + + + + + +); + +export const TabletIcon: React.FC> = props => ( + + + + +); + +export const SettingsIcon: React.FC> = props => ( + + + + +); + +export const UserIcon: React.FC> = props => ( + + + + +); + +export const CameraIcon: React.FC> = props => ( + + + + +); + +export const BellIcon: React.FC> = props => ( + + + + +); + +export const ZzzIcon: React.FC> = props => ( + + + +); + +export const WaterDropIcon: React.FC> = props => ( + + + +); + +export const CoffeeIcon: React.FC> = props => ( + + + + + + +); + +export const BarChartIcon: React.FC> = props => ( + + + + + +); + +export const medicationIcons: { + [key: string]: React.FC>; +} = { + pill: PillIcon, + tablet: TabletIcon, + capsule: CapsuleIcon, + syringe: SyringeIcon, + bottle: BottleIcon, +}; + +export const getMedicationIcon = ( + iconName?: string +): React.FC> => { + return (iconName && medicationIcons[iconName]) || PillIcon; +}; + +export const reminderIcons: { + [key: string]: React.FC>; +} = { + bell: BellIcon, + water: WaterDropIcon, + break: CoffeeIcon, +}; + +export const getReminderIcon = ( + iconName?: string +): React.FC> => { + return (iconName && reminderIcons[iconName]) || BellIcon; +}; diff --git a/components/medication/AddMedicationModal.tsx b/components/medication/AddMedicationModal.tsx new file mode 100644 index 0000000..419dece --- /dev/null +++ b/components/medication/AddMedicationModal.tsx @@ -0,0 +1,269 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Medication, Frequency } from '../../types'; +import { medicationIcons } from '../icons/Icons'; + +interface AddMedicationModalProps { + isOpen: boolean; + onClose: () => void; + onAdd: (medication: Omit) => Promise; +} + +const AddMedicationModal: React.FC = ({ + isOpen, + onClose, + onAdd, +}) => { + const [name, setName] = useState(''); + const [dosage, setDosage] = useState(''); + const [frequency, setFrequency] = useState(Frequency.Daily); + const [hoursBetween, setHoursBetween] = useState(8); + const [startTime, setStartTime] = useState('09:00'); + const [notes, setNotes] = useState(''); + const [icon, setIcon] = useState('pill'); + const [isSaving, setIsSaving] = useState(false); + + const modalRef = useRef(null); + const nameInputRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setName(''); + setDosage(''); + setFrequency(Frequency.Daily); + setHoursBetween(8); + setStartTime('09:00'); + setNotes(''); + setIcon('pill'); + setIsSaving(false); + setTimeout(() => nameInputRef.current?.focus(), 100); + } + }, [isOpen]); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && !isSaving) onClose(); + }; + if (isOpen) { + window.addEventListener('keydown', handleKeyDown); + } + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose, isSaving]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name || !dosage || !startTime || isSaving) { + return; + } + setIsSaving(true); + try { + await onAdd({ + name, + dosage, + frequency, + hoursBetween: + frequency === Frequency.EveryXHours ? hoursBetween : undefined, + startTime, + notes, + icon, + }); + } catch (error) { + console.error('Failed to add medication', error); + alert('There was an error saving your medication. Please try again.'); + setIsSaving(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ Add New Medication +

+
+
+
+
+ + setName(e.target.value)} + required + ref={nameInputRef} + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
+
+ + setDosage(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
+
+ + +
+ {frequency === Frequency.EveryXHours && ( +
+ + setHoursBetween(parseInt(e.target.value, 10))} + min='1' + max='23' + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
+ )} +
+ + setStartTime(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
+
+ +
+ {Object.entries(medicationIcons).map(([key, IconComponent]) => ( + + ))} +
+
+
+ + +
+
+
+ + +
+
+
+
+ ); +}; + +const Spinner = () => ( + + + + +); + +export default AddMedicationModal; diff --git a/components/medication/DoseCard.tsx b/components/medication/DoseCard.tsx new file mode 100644 index 0000000..c33413e --- /dev/null +++ b/components/medication/DoseCard.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { Medication, Dose, DoseStatus } from '../../types'; +import { + ClockIcon, + CheckCircleIcon, + XCircleIcon, + InfoIcon, + getMedicationIcon, + ZzzIcon, +} from '../icons/Icons'; + +interface DoseCardProps { + dose: Dose & { takenAt?: string }; + medication: Medication; + status: DoseStatus; + onToggleDose: (doseId: string) => void; + onSnooze: (doseId: string) => void; + snoozedUntil?: Date; +} + +const statusStyles = { + [DoseStatus.UPCOMING]: { + bg: 'bg-white dark:bg-slate-800', + icon: , + text: 'text-slate-500 dark:text-slate-400', + button: + 'border-indigo-600 text-indigo-600 hover:bg-indigo-600 hover:text-white dark:text-indigo-400 dark:border-indigo-400 dark:hover:bg-indigo-400 dark:hover:text-white', + buttonText: 'Take', + ring: 'hover:ring-indigo-300 dark:hover:ring-indigo-500', + }, + [DoseStatus.TAKEN]: { + bg: 'bg-green-50 dark:bg-green-900/20', + icon: ( + + ), + text: 'text-green-700 dark:text-green-400', + button: + 'border-green-500 text-green-500 hover:bg-green-500 hover:text-white dark:text-green-400 dark:border-green-400 dark:hover:bg-green-400 dark:hover:text-slate-900', + buttonText: 'Untake', + ring: '', + }, + [DoseStatus.MISSED]: { + bg: 'bg-red-50 dark:bg-red-900/20', + icon: , + text: 'text-red-700 dark:text-red-400', + button: + 'border-red-500 text-red-500 hover:bg-red-500 hover:text-white dark:text-red-400 dark:border-red-400 dark:hover:bg-red-400 dark:hover:text-slate-900', + buttonText: 'Take Now', + ring: '', + }, + [DoseStatus.SNOOZED]: { + bg: 'bg-amber-50 dark:bg-amber-900/20', + icon: , + text: 'text-amber-700 dark:text-amber-400', + button: + 'border-indigo-600 text-indigo-600 hover:bg-indigo-600 hover:text-white dark:text-indigo-400 dark:border-indigo-400 dark:hover:bg-indigo-400 dark:hover:text-white', + buttonText: 'Take', + ring: '', + }, +}; + +const DoseCard: React.FC = ({ + dose, + medication, + status, + onToggleDose, + onSnooze, + snoozedUntil, +}) => { + const styles = statusStyles[status]; + const timeString = dose.scheduledTime.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + const takenTimeString = dose.takenAt + ? new Date(dose.takenAt).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }) + : ''; + const snoozedTimeString = snoozedUntil + ? snoozedUntil.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }) + : ''; + const MedicationIcon = getMedicationIcon(medication.icon); + + return ( +
  • +
    +
    +
    + +
    +

    + {medication.name} +

    +

    + {medication.dosage} +

    +
    +
    + {styles.icon} +
    +
    + + {timeString} +
    + + {status === DoseStatus.SNOOZED && ( +

    + Snoozed until {snoozedTimeString} +

    + )} + + {status === DoseStatus.TAKEN && ( +

    + Taken at {takenTimeString} +

    + )} + + {medication.notes && ( +
    + +

    + {medication.notes} +

    +
    + )} +
    +
    + {status === DoseStatus.UPCOMING && ( + + )} + +
    +
  • + ); +}; + +export default DoseCard; diff --git a/components/medication/EditMedicationModal.tsx b/components/medication/EditMedicationModal.tsx new file mode 100644 index 0000000..0a3a5e1 --- /dev/null +++ b/components/medication/EditMedicationModal.tsx @@ -0,0 +1,274 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Medication, Frequency } from '../../types'; +import { medicationIcons } from '../icons/Icons'; + +interface EditMedicationModalProps { + isOpen: boolean; + onClose: () => void; + medication: Medication | null; + onUpdate: (medication: Medication) => Promise; +} + +const EditMedicationModal: React.FC = ({ + isOpen, + onClose, + medication, + onUpdate, +}) => { + const [name, setName] = useState(''); + const [dosage, setDosage] = useState(''); + const [frequency, setFrequency] = useState(Frequency.Daily); + const [hoursBetween, setHoursBetween] = useState(8); + const [startTime, setStartTime] = useState('09:00'); + const [notes, setNotes] = useState(''); + const [icon, setIcon] = useState('pill'); + const [isSaving, setIsSaving] = useState(false); + + const modalRef = useRef(null); + const nameInputRef = useRef(null); + + useEffect(() => { + if (medication) { + setName(medication.name); + setDosage(medication.dosage); + setFrequency(medication.frequency); + setHoursBetween(medication.hoursBetween || 8); + setStartTime(medication.startTime); + setNotes(medication.notes || ''); + setIcon(medication.icon || 'pill'); + setIsSaving(false); + } + if (isOpen) { + setTimeout(() => nameInputRef.current?.focus(), 100); + } + }, [medication, isOpen]); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && !isSaving) onClose(); + }; + if (isOpen) { + window.addEventListener('keydown', handleKeyDown); + } + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose, isSaving]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!medication || !name || !dosage || !startTime || isSaving) { + return; + } + setIsSaving(true); + try { + await onUpdate({ + ...medication, + name, + dosage, + frequency, + hoursBetween: + frequency === Frequency.EveryXHours ? hoursBetween : undefined, + startTime, + notes, + icon, + }); + } catch (error) { + console.error('Failed to update medication', error); + alert('There was an error updating your medication. Please try again.'); + setIsSaving(false); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    +

    + Edit Medication +

    +
    +
    +
    +
    + + setName(e.target.value)} + required + ref={nameInputRef} + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
    +
    + + setDosage(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
    +
    + + +
    + {frequency === Frequency.EveryXHours && ( +
    + + setHoursBetween(parseInt(e.target.value, 10))} + min='1' + max='23' + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
    + )} +
    + + setStartTime(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
    +
    + +
    + {Object.entries(medicationIcons).map(([key, IconComponent]) => ( + + ))} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    + ); +}; + +const Spinner = () => ( + + + + +); + +export default EditMedicationModal; diff --git a/components/medication/ManageMedicationsModal.tsx b/components/medication/ManageMedicationsModal.tsx new file mode 100644 index 0000000..22c61d9 --- /dev/null +++ b/components/medication/ManageMedicationsModal.tsx @@ -0,0 +1,177 @@ +import React, { useMemo, useEffect, useRef } from 'react'; +import { Medication } from '../../types'; +import { TrashIcon, EditIcon, getMedicationIcon } from '../icons/Icons'; + +interface ManageMedicationsModalProps { + isOpen: boolean; + onClose: () => void; + medications: Medication[]; + // FIX: Changed onDelete to expect the full medication object to match the parent's handler. + onDelete: (medication: Medication) => void; + onEdit: (medication: Medication) => void; +} + +const ManageMedicationsModal: React.FC = ({ + isOpen, + onClose, + medications, + onDelete, + onEdit, +}) => { + const modalRef = useRef(null); + const closeButtonRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setTimeout(() => closeButtonRef.current?.focus(), 100); + } + }, [isOpen]); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + if (isOpen) { + window.addEventListener('keydown', handleKeyDown); + } + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + useEffect(() => { + if (!isOpen || !modalRef.current) return; + const focusableElements = modalRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[ + focusableElements.length - 1 + ] as HTMLElement; + + const handleTabKey = (e: KeyboardEvent) => { + if (e.key !== 'Tab') return; + if (e.shiftKey) { + if (document.activeElement === firstElement) { + lastElement.focus(); + e.preventDefault(); + } + } else { + if (document.activeElement === lastElement) { + firstElement.focus(); + e.preventDefault(); + } + } + }; + + document.addEventListener('keydown', handleTabKey); + return () => document.removeEventListener('keydown', handleTabKey); + }, [isOpen]); + + const sortedMedications = useMemo( + () => [...medications].sort((a, b) => a.name.localeCompare(b.name)), + [medications] + ); + + const handleDeleteConfirmation = (medication: Medication) => { + if (window.confirm(`Are you sure you want to delete ${medication.name}?`)) { + // FIX: Pass the whole medication object to the onDelete handler. + onDelete(medication); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    +

    + Manage Medications +

    + +
    +
    + {sortedMedications.length > 0 ? ( +
      + {sortedMedications.map(med => { + const MedicationIcon = getMedicationIcon(med.icon); + return ( + // FIX: The Medication type has `_id`, not `id`. Used for the key. +
    • +
      + +
      +

      + {med.name} +

      +

      + {med.dosage} • {med.frequency} +

      + {med.notes && ( +

      + Note: "{med.notes}" +

      + )} +
      +
      +
      + + +
      +
    • + ); + })} +
    + ) : ( +

    + No medications have been added yet. +

    + )} +
    +
    + +
    +
    +
    + ); +}; + +export default ManageMedicationsModal; diff --git a/components/medication/index.ts b/components/medication/index.ts new file mode 100644 index 0000000..687af3b --- /dev/null +++ b/components/medication/index.ts @@ -0,0 +1,5 @@ +// Medication Components +export { default as AddMedicationModal } from './AddMedicationModal'; +export { default as EditMedicationModal } from './EditMedicationModal'; +export { default as ManageMedicationsModal } from './ManageMedicationsModal'; +export { default as DoseCard } from './DoseCard'; diff --git a/components/modals/AccountModal.tsx b/components/modals/AccountModal.tsx new file mode 100644 index 0000000..407f554 --- /dev/null +++ b/components/modals/AccountModal.tsx @@ -0,0 +1,301 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { User, UserSettings } from '../../types'; +import { CameraIcon, TrashIcon, UserIcon } from '../icons/Icons'; + +interface AccountModalProps { + isOpen: boolean; + onClose: () => void; + user: User; + settings: UserSettings; + onUpdateUser: (user: User) => Promise; + onUpdateSettings: (settings: UserSettings) => Promise; + onDeleteAllData: () => Promise; +} + +const AccountModal: React.FC = ({ + isOpen, + onClose, + user, + settings, + onUpdateUser, + onUpdateSettings, + onDeleteAllData, +}) => { + const [username, setUsername] = useState(user.username); + const [successMessage, setSuccessMessage] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const modalRef = useRef(null); + const closeButtonRef = useRef(null); + const fileInputRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setUsername(user.username); + setSuccessMessage(''); + setTimeout(() => closeButtonRef.current?.focus(), 100); + } + }, [isOpen, user.username]); + + const handleUsernameSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (username.trim() && username !== user.username) { + setIsSaving(true); + try { + await onUpdateUser({ ...user, username: username.trim() }); + setSuccessMessage('Username updated successfully!'); + setTimeout(() => setSuccessMessage(''), 3000); + } catch (error) { + alert('Failed to update username.'); + } finally { + setIsSaving(false); + } + } + }; + + const handleToggleNotifications = ( + e: React.ChangeEvent + ) => { + onUpdateSettings({ ...settings, notificationsEnabled: e.target.checked }); + }; + + const handleAvatarChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = async () => { + setIsSaving(true); + try { + await onUpdateUser({ ...user, avatar: reader.result as string }); + } finally { + setIsSaving(false); + } + }; + reader.readAsDataURL(file); + } + }; + + const handleRemoveAvatar = async () => { + const { avatar, ...userWithoutAvatar } = user; + setIsSaving(true); + try { + await onUpdateUser(userWithoutAvatar); + } finally { + setIsSaving(false); + } + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + await onDeleteAllData(); + } catch (error) { + alert('Failed to delete data.'); + } finally { + setIsDeleting(false); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    +

    + Account Settings +

    + +
    +
    +
    +

    + Profile +

    +
    +
    + {user.avatar ? ( + User avatar + ) : ( + + + + )} + + +
    +
    + + {user.avatar && ( + + )} +
    +
    +
    +
    + +
    + setUsername(e.target.value)} + className='flex-1 block w-full min-w-0 rounded-none rounded-l-md px-3 py-2 border border-slate-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> + +
    + {successMessage && ( +

    + {successMessage} +

    + )} +
    +
    +
    + +
    +

    + Preferences +

    +
    + + Enable Notifications + + +
    +
    + +
    +

    + Danger Zone +

    +
    +
    +
    +

    + Delete All Data +

    +

    + Permanently delete all your medications and history. +

    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + ); +}; + +const Spinner = () => ( + + + + +); + +export default AccountModal; diff --git a/components/modals/AddReminderModal.tsx b/components/modals/AddReminderModal.tsx new file mode 100644 index 0000000..fdf378a --- /dev/null +++ b/components/modals/AddReminderModal.tsx @@ -0,0 +1,193 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { CustomReminder } from '../../types'; +import { reminderIcons } from '../icons/Icons'; + +interface AddReminderModalProps { + isOpen: boolean; + onClose: () => void; + onAdd: (reminder: Omit) => Promise; +} + +const AddReminderModal: React.FC = ({ + isOpen, + onClose, + onAdd, +}) => { + const [title, setTitle] = useState(''); + const [icon, setIcon] = useState('bell'); + const [frequencyMinutes, setFrequencyMinutes] = useState(60); + const [startTime, setStartTime] = useState('09:00'); + const [endTime, setEndTime] = useState('17:00'); + const [isSaving, setIsSaving] = useState(false); + + const titleInputRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setTitle(''); + setIcon('bell'); + setFrequencyMinutes(60); + setStartTime('09:00'); + setEndTime('17:00'); + setIsSaving(false); + setTimeout(() => titleInputRef.current?.focus(), 100); + } + }, [isOpen]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!title || isSaving) return; + + setIsSaving(true); + try { + await onAdd({ + title, + icon, + frequencyMinutes, + startTime, + endTime, + }); + } catch (error) { + console.error('Failed to add reminder', error); + alert('There was an error saving your reminder. Please try again.'); + } finally { + setIsSaving(false); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    +

    + Add New Reminder +

    +
    +
    +
    +
    + + setTitle(e.target.value)} + required + ref={titleInputRef} + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + placeholder='e.g., Drink water' + /> +
    + +
    + +
    + {Object.entries(reminderIcons).map(([key, IconComponent]) => ( + + ))} +
    +
    + +
    + + + setFrequencyMinutes(parseInt(e.target.value, 10)) + } + min='1' + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600' + /> +
    + +
    +
    + + setStartTime(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600' + /> +
    +
    + + setEndTime(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600' + /> +
    +
    +
    +
    + + +
    +
    +
    +
    + ); +}; + +export default AddReminderModal; diff --git a/components/modals/EditReminderModal.tsx b/components/modals/EditReminderModal.tsx new file mode 100644 index 0000000..d3488e4 --- /dev/null +++ b/components/modals/EditReminderModal.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { CustomReminder } from '../../types'; +import { reminderIcons } from '../icons/Icons'; + +interface EditReminderModalProps { + isOpen: boolean; + onClose: () => void; + reminder: CustomReminder | null; + onUpdate: (reminder: CustomReminder) => Promise; +} + +const EditReminderModal: React.FC = ({ + isOpen, + onClose, + reminder, + onUpdate, +}) => { + const [title, setTitle] = useState(''); + const [icon, setIcon] = useState('bell'); + const [frequencyMinutes, setFrequencyMinutes] = useState(60); + const [startTime, setStartTime] = useState('09:00'); + const [endTime, setEndTime] = useState('17:00'); + const [isSaving, setIsSaving] = useState(false); + + const titleInputRef = useRef(null); + + useEffect(() => { + if (isOpen && reminder) { + setTitle(reminder.title); + setIcon(reminder.icon); + setFrequencyMinutes(reminder.frequencyMinutes); + setStartTime(reminder.startTime); + setEndTime(reminder.endTime); + setIsSaving(false); + setTimeout(() => titleInputRef.current?.focus(), 100); + } + }, [isOpen, reminder]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!title || !reminder || isSaving) return; + + setIsSaving(true); + try { + await onUpdate({ + ...reminder, + title, + icon, + frequencyMinutes, + startTime, + endTime, + }); + } catch (error) { + console.error('Failed to update reminder', error); + alert('There was an error updating your reminder. Please try again.'); + } finally { + setIsSaving(false); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    +

    + Edit Reminder +

    +
    +
    +
    +
    + + setTitle(e.target.value)} + required + ref={titleInputRef} + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white' + /> +
    + +
    + +
    + {Object.entries(reminderIcons).map(([key, IconComponent]) => ( + + ))} +
    +
    + +
    + + + setFrequencyMinutes(parseInt(e.target.value, 10)) + } + min='1' + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600' + /> +
    + +
    +
    + + setStartTime(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600' + /> +
    +
    + + setEndTime(e.target.value)} + required + className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600' + /> +
    +
    +
    +
    + + +
    +
    +
    +
    + ); +}; + +export default EditReminderModal; diff --git a/components/modals/HistoryModal.tsx b/components/modals/HistoryModal.tsx new file mode 100644 index 0000000..0e09d50 --- /dev/null +++ b/components/modals/HistoryModal.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useRef } from 'react'; +import { HistoricalDose } from '../../types'; +import { + PillIcon, + CheckCircleIcon, + XCircleIcon, + ClockIcon, +} from '../icons/Icons'; + +interface HistoryModalProps { + isOpen: boolean; + onClose: () => void; + history: { date: string; doses: HistoricalDose[] }[]; +} + +const getStatusIcon = (status: HistoricalDose['status']) => { + switch (status) { + case 'TAKEN': + return ( + + ); + case 'MISSED': + return ; + default: + return ( + + ); + } +}; + +const HistoryModal: React.FC = ({ + isOpen, + onClose, + history, +}) => { + const modalRef = useRef(null); + const closeButtonRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setTimeout(() => closeButtonRef.current?.focus(), 100); + } + }, [isOpen]); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + if (isOpen) { + window.addEventListener('keydown', handleKeyDown); + } + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + useEffect(() => { + if (!isOpen || !modalRef.current) return; + const focusableElements = modalRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[ + focusableElements.length - 1 + ] as HTMLElement; + + const handleTabKey = (e: KeyboardEvent) => { + if (e.key !== 'Tab') return; + if (e.shiftKey) { + if (document.activeElement === firstElement) { + lastElement.focus(); + e.preventDefault(); + } + } else { + if (document.activeElement === lastElement) { + firstElement.focus(); + e.preventDefault(); + } + } + }; + + document.addEventListener('keydown', handleTabKey); + return () => document.removeEventListener('keydown', handleTabKey); + }, [isOpen]); + + if (!isOpen) return null; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const userTimezoneOffset = date.getTimezoneOffset() * 60000; + return new Date(date.getTime() + userTimezoneOffset).toLocaleDateString( + undefined, + { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + } + ); + }; + + return ( +
    +
    +
    +

    + Medication History +

    + +
    +
    + {history.length > 0 ? ( + history.map(({ date, doses }) => ( +
    +

    + {formatDate(date)} +

    +
      + {doses.map(dose => ( +
    • +
      +
      + {getStatusIcon(dose.status)} +
      +
      +

      + {dose.medication.name} +

      +

      + {dose.medication.dosage} +

      +
      +
      +
      +

      + {dose.scheduledTime.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} +

      + {dose.status === 'TAKEN' && dose.takenAt && ( +

      + Taken at{' '} + {new Date(dose.takenAt).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} +

      + )} + {dose.status === 'MISSED' && ( +

      + Missed +

      + )} +
      +
    • + ))} +
    +
    + )) + ) : ( +
    + +

    + No medication history found. +

    +

    + History will appear here once you start tracking doses. +

    +
    + )} +
    +
    + +
    +
    +
    + ); +}; + +export default HistoryModal; diff --git a/components/modals/ManageRemindersModal.tsx b/components/modals/ManageRemindersModal.tsx new file mode 100644 index 0000000..edb73c0 --- /dev/null +++ b/components/modals/ManageRemindersModal.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { CustomReminder } from '../../types'; +import { TrashIcon, EditIcon, PlusIcon, getReminderIcon } from '../icons/Icons'; + +interface ManageRemindersModalProps { + isOpen: boolean; + onClose: () => void; + reminders: CustomReminder[]; + onAdd: () => void; + onDelete: (reminder: CustomReminder) => void; + onEdit: (reminder: CustomReminder) => void; +} + +const ManageRemindersModal: React.FC = ({ + isOpen, + onClose, + reminders, + onAdd, + onDelete, + onEdit, +}) => { + const handleDeleteConfirmation = (reminder: CustomReminder) => { + if ( + window.confirm( + `Are you sure you want to delete the reminder "${reminder.title}"?` + ) + ) { + onDelete(reminder); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    +

    + Manage Custom Reminders +

    + +
    +
    + {reminders.length > 0 ? ( +
      + {reminders.map(rem => { + const ReminderIcon = getReminderIcon(rem.icon); + return ( +
    • +
      + +
      +

      + {rem.title} +

      +

      + Every {rem.frequencyMinutes} mins from {rem.startTime}{' '} + to {rem.endTime} +

      +
      +
      +
      + + +
      +
    • + ); + })} +
    + ) : ( +

    + No custom reminders have been added yet. +

    + )} +
    +
    + + +
    +
    +
    + ); +}; + +export default ManageRemindersModal; diff --git a/components/modals/OnboardingModal.tsx b/components/modals/OnboardingModal.tsx new file mode 100644 index 0000000..0f08353 --- /dev/null +++ b/components/modals/OnboardingModal.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { PillIcon, PlusIcon, CheckCircleIcon } from './icons/Icons'; + +interface OnboardingModalProps { + isOpen: boolean; + onComplete: () => void; +} + +const onboardingSteps = [ + { + icon: PillIcon, + title: 'Welcome to Medication Reminder!', + description: + 'This quick tour will show you how to get the most out of the app.', + }, + { + icon: PlusIcon, + title: 'Add Your Medications', + description: + "Start by clicking the 'Add Medication' button. You can set the name, dosage, frequency, and a custom icon.", + }, + { + icon: CheckCircleIcon, + title: 'Track Your Doses', + description: + "Your daily schedule will appear on the main screen. Simply tap 'Take' to record a dose and stay on track with your health.", + }, +]; + +const OnboardingModal: React.FC = ({ + isOpen, + onComplete, +}) => { + const [step, setStep] = useState(0); + const currentStep = onboardingSteps[step]; + const isLastStep = step === onboardingSteps.length - 1; + + const handleNext = () => { + if (isLastStep) { + onComplete(); + } else { + setStep(s => s + 1); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    +
    + +
    +

    + {currentStep.title} +

    +

    + {currentStep.description} +

    + +
    + {onboardingSteps.map((_, index) => ( +
    + ))} +
    + + +
    +
    + ); +}; + +export default OnboardingModal; diff --git a/components/modals/StatsModal.tsx b/components/modals/StatsModal.tsx new file mode 100644 index 0000000..f7435e6 --- /dev/null +++ b/components/modals/StatsModal.tsx @@ -0,0 +1,244 @@ +import React, { useEffect, useRef } from 'react'; +import { DailyStat, MedicationStat } from '../../types'; +import BarChart from '../ui/BarChart'; +import { BarChartIcon, getMedicationIcon } from '../icons/Icons'; + +interface StatsModalProps { + isOpen: boolean; + onClose: () => void; + dailyStats: DailyStat[]; + medicationStats: MedicationStat[]; +} + +const formatLastTaken = (isoString?: string) => { + if (!isoString) + return N/A; + + const date = new Date(isoString); + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() - 1 + ); + + const timeString = date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + const datePart = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() + ); + + if (datePart.getTime() === today.getTime()) { + return `Today at ${timeString}`; + } + if (datePart.getTime() === yesterday.getTime()) { + return `Yesterday at ${timeString}`; + } + return date.toLocaleDateString(); +}; + +const StatsModal: React.FC = ({ + isOpen, + onClose, + dailyStats, + medicationStats, +}) => { + const modalRef = useRef(null); + const closeButtonRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setTimeout(() => closeButtonRef.current?.focus(), 100); + } + }, [isOpen]); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + if (isOpen) { + window.addEventListener('keydown', handleKeyDown); + } + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + const hasData = medicationStats.length > 0; + + return ( +
    +
    +
    +

    + Medication Statistics +

    + +
    +
    + {hasData ? ( + <> +
    +

    + Weekly Adherence +

    +
    + +
    +
    +
    +

    + Medication Breakdown +

    +
    + + + + + + + + + + + + + {medicationStats.map( + ({ + medication, + taken, + missed, + upcoming, + adherence, + lastTakenAt, + }) => { + const MedicationIcon = getMedicationIcon( + medication.icon + ); + const adherenceColor = + adherence >= 90 + ? 'text-green-600 dark:text-green-400' + : adherence >= 70 + ? 'text-amber-600 dark:text-amber-400' + : 'text-red-600 dark:text-red-400'; + return ( + + + + + + + + + ); + } + )} + +
    + Medication + + Taken + + Missed + + Upcoming + + Last Taken + + Adherence +
    +
    + +
    +
    + {medication.name} +
    +
    + {medication.dosage} +
    +
    +
    +
    + {taken} + + {missed} + + {upcoming} + + {formatLastTaken(lastTakenAt)} + + {adherence}% +
    +
    +
    + + ) : ( +
    + +

    + Not enough data to display stats. +

    +

    + Statistics will appear here once you start tracking your doses. +

    +
    + )} +
    +
    + +
    +
    +
    + ); +}; + +export default StatsModal; diff --git a/components/modals/index.ts b/components/modals/index.ts new file mode 100644 index 0000000..acc6d6f --- /dev/null +++ b/components/modals/index.ts @@ -0,0 +1,8 @@ +// Modal Components +export { default as AccountModal } from './AccountModal'; +export { default as AddReminderModal } from './AddReminderModal'; +export { default as EditReminderModal } from './EditReminderModal'; +export { default as HistoryModal } from './HistoryModal'; +export { default as ManageRemindersModal } from './ManageRemindersModal'; +export { default as OnboardingModal } from './OnboardingModal'; +export { default as StatsModal } from './StatsModal'; diff --git a/components/ui/BarChart.tsx b/components/ui/BarChart.tsx new file mode 100644 index 0000000..0e2af44 --- /dev/null +++ b/components/ui/BarChart.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { DailyStat } from '../../types'; + +interface BarChartProps { + data: DailyStat[]; +} + +const BarChart: React.FC = ({ data }) => { + const chartHeight = 150; + const barWidth = 30; + const barMargin = 15; + const chartWidth = data.length * (barWidth + barMargin); + + const getDayLabel = (dateString: string) => { + const date = new Date(dateString); + const userTimezoneOffset = date.getTimezoneOffset() * 60000; + const adjustedDate = new Date(date.getTime() + userTimezoneOffset); + return adjustedDate.toLocaleDateString('en-US', { weekday: 'short' }); + }; + + return ( +
    + + Weekly Medication Adherence Chart + + {/* Y-Axis Labels */} + + + 100% + + + 50% + + + 0% + + + + {/* Y-Axis Grid Lines */} + + + + + {data.map((item, index) => { + const x = index * (barWidth + barMargin); + const barHeight = (item.adherence / 100) * (chartHeight - 10); + const y = chartHeight - barHeight; + + const barColorClass = + item.adherence >= 90 + ? 'fill-current text-green-500 dark:text-green-400' + : item.adherence >= 70 + ? 'fill-current text-amber-500 dark:text-amber-400' + : 'fill-current text-red-500 dark:text-red-400'; + + return ( + + + + {getDayLabel(item.date)}: {item.adherence}% adherence + + + + {getDayLabel(item.date)} + + + ); + })} + +
    + ); +}; + +export default BarChart; diff --git a/components/ui/ReminderCard.tsx b/components/ui/ReminderCard.tsx new file mode 100644 index 0000000..c20e91f --- /dev/null +++ b/components/ui/ReminderCard.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { ReminderInstance } from '../../types'; +import { ClockIcon, getReminderIcon } from '../icons/Icons'; + +interface ReminderCardProps { + reminder: ReminderInstance; +} + +const ReminderCard: React.FC = ({ reminder }) => { + const timeString = reminder.scheduledTime.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + const ReminderIcon = getReminderIcon(reminder.icon); + + return ( +
  • +
    +
    +
    + +
    +

    + {reminder.title} +

    +
    +
    +
    +
    + + {timeString} +
    +
    +
  • + ); +}; + +export default ReminderCard; diff --git a/components/ui/ThemeSwitcher.tsx b/components/ui/ThemeSwitcher.tsx new file mode 100644 index 0000000..c71b556 --- /dev/null +++ b/components/ui/ThemeSwitcher.tsx @@ -0,0 +1,74 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useTheme } from '../../hooks/useTheme'; +import { SunIcon, MoonIcon, DesktopIcon } from '../icons/Icons'; + +type Theme = 'light' | 'dark' | 'system'; + +const themeOptions: { + value: Theme; + label: string; + icon: React.FC>; +}[] = [ + { value: 'light', label: 'Light', icon: SunIcon }, + { value: 'dark', label: 'Dark', icon: MoonIcon }, + { value: 'system', label: 'System', icon: DesktopIcon }, +]; + +const ThemeSwitcher: React.FC = () => { + const { theme, setTheme } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const currentTheme = + themeOptions.find(t => t.value === theme) || themeOptions[2]; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
    + + + {isOpen && ( +
    + {themeOptions.map(option => ( + + ))} +
    + )} +
    + ); +}; + +export default ThemeSwitcher; diff --git a/components/ui/index.ts b/components/ui/index.ts new file mode 100644 index 0000000..8afd751 --- /dev/null +++ b/components/ui/index.ts @@ -0,0 +1,4 @@ +// UI Components +export { default as BarChart } from './BarChart'; +export { default as ReminderCard } from './ReminderCard'; +export { default as ThemeSwitcher } from './ThemeSwitcher'; diff --git a/contexts/UserContext.tsx b/contexts/UserContext.tsx new file mode 100644 index 0000000..4fb8a4f --- /dev/null +++ b/contexts/UserContext.tsx @@ -0,0 +1,219 @@ +import React, { + createContext, + useContext, + useState, + useEffect, + ReactNode, +} from 'react'; +import { User } from '../types'; +import { dbService } from '../services/couchdb.factory'; +import { authService } from '../services/auth/auth.service'; + +const SESSION_KEY = 'medication_app_session'; + +interface UserContextType { + user: User | null; + isLoading: boolean; + login: (email: string, password: string) => Promise; + register: ( + email: string, + password: string, + username?: string + ) => Promise; + loginWithOAuth: ( + provider: 'google' | 'github', + userData: any + ) => Promise; + changePassword: ( + currentPassword: string, + newPassword: string + ) => Promise; + logout: () => void; + updateUser: ( + updatedUser: Omit & { _rev: string } + ) => Promise; +} + +const UserContext = createContext(undefined); + +export const UserProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + try { + const sessionUser = localStorage.getItem(SESSION_KEY); + if (sessionUser) { + setUser(JSON.parse(sessionUser)); + } + } catch { + // silent fail + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + if (user) { + localStorage.setItem(SESSION_KEY, JSON.stringify(user)); + } else { + localStorage.removeItem(SESSION_KEY); + } + }, [user]); + + const login = async (email: string, password: string): Promise => { + try { + // Use auth service for password-based login + const result = await authService.login({ email, password }); + + console.log('Login result received:', result); + console.log('User from login:', result.user); + console.log('User _id:', result.user._id); + + // Update last login time + const updatedUser = { ...result.user, lastLoginAt: new Date() }; + await dbService.updateUser(updatedUser); + + console.log('Updated user with last login:', updatedUser); + + // Store access token for subsequent API calls. + localStorage.setItem('access_token', result.accessToken); + // Set the user from the login result + setUser(updatedUser); + + console.log('User set in context'); + return true; + } catch (error) { + console.error('Login error:', error); + return false; + } + }; + + const register = async ( + email: string, + password: string, + username?: string + ): Promise => { + try { + const result = await authService.register(email, password, username); + // Don't auto-login after registration, require email verification + return true; + } catch (error) { + console.error('Registration error:', error); + return false; + } + }; + + const loginWithOAuth = async ( + provider: 'google' | 'github', + userData: any + ): Promise => { + try { + const result = await authService.loginWithOAuth(provider, userData); + + console.log('OAuth login result received:', result); + console.log('OAuth user:', result.user); + console.log('OAuth user _id:', result.user._id); + + // Update last login time + const updatedUser = { ...result.user, lastLoginAt: new Date() }; + await dbService.updateUser(updatedUser); + + console.log('Updated OAuth user with last login:', updatedUser); + + localStorage.setItem('access_token', result.accessToken); + setUser(updatedUser); + + console.log('OAuth user set in context'); + return true; + } catch (error) { + console.error('OAuth login error:', error); + return false; + } + }; + + const changePassword = async ( + currentPassword: string, + newPassword: string + ): Promise => { + try { + if (!user) { + throw new Error('No user logged in'); + } + + await authService.changePassword(user._id, currentPassword, newPassword); + return true; + } catch (error) { + console.error('Password change error:', error); + return false; + } + }; + + const logout = () => { + setUser(null); + }; + + const updateUser = async (updatedUser: User) => { + try { + const savedUser = await dbService.updateUser(updatedUser); + setUser(savedUser); + } catch (error) { + console.error('Failed to update user', error); + // Optionally revert state or show error + } + }; + + if (isLoading) { + return ( +
    + +
    + ); + } + + return ( + + {children} + + ); +}; + +export const useUser = (): UserContextType => { + const context = useContext(UserContext); + if (context === undefined) { + throw new Error('useUser must be used within a UserProvider'); + } + return context; +}; + +// Dummy icon for loading screen +const PillIcon: React.FC> = props => ( + + + + +); diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..1a2ca53 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,78 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build output +dist +build + +# Environment files (security) +.env +.env.* +!.env.example + +# Development files +.vscode +.idea +*.swp +*.swo +*~ + +# Version control +.git +.gitignore +.gitattributes + +# Documentation +README.md +README_*.md +CHANGELOG.md +CONTRIBUTING.md +LICENSE +docs/ + +# Docker files (avoid recursion) +Dockerfile +docker-compose.yaml +.dockerignore + +# Scripts and testing (not needed in container) +scripts/ +tests/ +coverage/ +**/__tests__ +**/*.test.* +**/*.spec.* + +# Logs +logs +*.log + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +tmp/ +temp/ +.tmp + +# CouchDB data +couchdb-data/ + +# Scripts (not needed in container) +setup.sh +deploy.sh +deploy-k8s.sh +validate-env.sh +validate-deployment.sh + +# Kubernetes manifests +k8s/ diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..48b807d --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,81 @@ +# check=skip=SecretsUsedInArgOrEnv +# Build stage +FROM oven/bun:alpine AS builder + +# Install system dependencies for native modules +RUN apk add --no-cache python3 make g++ + +# Create non-root user for security +RUN addgroup -g 1001 -S nodeuser && adduser -S nodeuser -u 1001 -G nodeuser + +# Create and set permissions for the working directory +RUN mkdir -p /app && chown -R nodeuser:nodeuser /app +WORKDIR /app +USER nodeuser + +# Copy package files first for better Docker layer caching +COPY --chown=nodeuser:nodeuser package.json ./ +COPY --chown=nodeuser:nodeuser bun.lock ./ + +# Install dependencies +RUN bun install --frozen-lockfile + +# Copy source code +COPY --chown=nodeuser:nodeuser . ./ + +# Build arguments for environment configuration +# CouchDB Configuration +ARG VITE_COUCHDB_URL=http://localhost:5984 +ARG VITE_COUCHDB_USER=admin +ARG VITE_COUCHDB_PASSWORD=change-this-secure-password + +# Application Configuration +ARG APP_BASE_URL=http://localhost:5173 + +# OAuth Configuration (Optional) +ARG VITE_GOOGLE_CLIENT_ID="" +ARG VITE_GITHUB_CLIENT_ID="" + +# Build Environment +ARG NODE_ENV=production + +# Set environment variables for build process +# These are embedded into the static build at compile time +ENV VITE_COUCHDB_URL=$VITE_COUCHDB_URL +ENV VITE_COUCHDB_USER=$VITE_COUCHDB_USER +ENV VITE_COUCHDB_PASSWORD=$VITE_COUCHDB_PASSWORD +ENV APP_BASE_URL=$APP_BASE_URL +ENV VITE_GOOGLE_CLIENT_ID=$VITE_GOOGLE_CLIENT_ID +ENV VITE_GITHUB_CLIENT_ID=$VITE_GITHUB_CLIENT_ID +ENV NODE_ENV=$NODE_ENV + +# Build the application +RUN bun run build + +# Production stage - serve with nginx +FROM nginx:alpine + +# Install curl for health checks +RUN apk add --no-cache curl + +# Copy built files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY --from=builder /app/docker/nginx.conf /etc/nginx/conf.d/default.conf + +# Set proper permissions for nginx +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d + +# Add health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/ || exit 1 + +# Expose port 80 +EXPOSE 80 + +# Start nginx (runs as nginx user by default in alpine) +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..ef178e6 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,76 @@ +# 🐳 Docker Configuration + +This directory contains all Docker and containerization-related files for RxMinder. + +## Files + +- **`Dockerfile`** - Multi-stage Docker build configuration with buildx support +- **`docker-compose.yaml`** - Service orchestration with multi-platform support +- **`docker-bake.hcl`** - Advanced buildx configuration for multi-platform builds +- **`nginx.conf`** - Production web server configuration +- **`.dockerignore`** - Files and directories to exclude from Docker build context + +## Docker Buildx Support + +This project now supports Docker Buildx for multi-platform builds (AMD64 and ARM64). + +### Quick Start with Buildx + +```bash +# Setup buildx builder (run once) +../scripts/buildx-helper.sh setup + +# Build for local platform only (faster for development) +../scripts/buildx-helper.sh build-local + +# Build for multiple platforms +../scripts/buildx-helper.sh build-multi + +# Build and push to registry +../scripts/buildx-helper.sh push docker.io/username latest + +# Build using Docker Bake (advanced) +../scripts/buildx-helper.sh bake +``` + +### Manual Buildx Commands + +```bash +# Create and use buildx builder +docker buildx create --name rxminder-builder --driver docker-container --bootstrap --use + +# Build for multiple platforms +docker buildx build --platform linux/amd64,linux/arm64 -t rxminder:latest --load . + +# Build with bake file +docker buildx bake -f docker-bake.hcl +``` + +## Traditional Usage + +From the project root directory: + +```bash +# Build and start services +docker compose -f docker/docker-compose.yaml up -d + +# View logs +docker compose -f docker/docker-compose.yaml logs + +# Stop services +docker compose -f docker/docker-compose.yaml down +``` + +## Build Process + +The Dockerfile uses a multi-stage build: + +1. **Builder stage**: Installs dependencies and builds the React app +2. **Production stage**: Serves the built app with nginx + +## Services + +- **frontend**: React application served by nginx +- **couchdb**: Database for medication and user data + +Both services include health checks and proper security configurations. diff --git a/docker/docker-bake.hcl b/docker/docker-bake.hcl new file mode 100644 index 0000000..23c1027 --- /dev/null +++ b/docker/docker-bake.hcl @@ -0,0 +1,101 @@ +# Docker Bake file for advanced multi-platform builds +# Usage: docker buildx bake -f docker-bake.hcl + +variable "TAG" { + default = "latest" +} + +variable "REGISTRY" { + default = "" +} + +variable "VITE_COUCHDB_URL" { + default = "http://localhost:5984" +} + +variable "VITE_COUCHDB_USER" { + default = "admin" +} + +variable "VITE_COUCHDB_PASSWORD" { + default = "change-this-secure-password" +} + +variable "APP_BASE_URL" { + default = "http://localhost:8080" +} + +variable "VITE_GOOGLE_CLIENT_ID" { + default = "" +} + +variable "VITE_GITHUB_CLIENT_ID" { + default = "" +} + +group "default" { + targets = ["app"] +} + +target "app" { + dockerfile = "Dockerfile" + context = "." + platforms = [ + "linux/amd64", + "linux/arm64" + ] + + tags = [ + "${REGISTRY}rxminder:${TAG}", + "${REGISTRY}rxminder:latest" + ] + + args = { + # CouchDB Configuration + VITE_COUCHDB_URL = "${VITE_COUCHDB_URL}" + VITE_COUCHDB_USER = "${VITE_COUCHDB_USER}" + VITE_COUCHDB_PASSWORD = "${VITE_COUCHDB_PASSWORD}" + + # Application Configuration + APP_BASE_URL = "${APP_BASE_URL}" + + # OAuth Configuration (Optional) + VITE_GOOGLE_CLIENT_ID = "${VITE_GOOGLE_CLIENT_ID}" + VITE_GITHUB_CLIENT_ID = "${VITE_GITHUB_CLIENT_ID}" + + # Build environment + NODE_ENV = "production" + } + + # Advanced buildx features + cache-from = [ + "type=gha", + "type=registry,ref=${REGISTRY}rxminder:buildcache" + ] + + cache-to = [ + "type=gha,mode=max", + "type=registry,ref=${REGISTRY}rxminder:buildcache,mode=max" + ] + + # Attestations for supply chain security + attest = [ + "type=provenance,mode=max", + "type=sbom" + ] +} + +# Development target for faster local builds +target "dev" { + inherits = ["app"] + platforms = ["linux/amd64"] + tags = ["rxminder:dev"] + cache-from = ["type=gha"] + cache-to = ["type=gha,mode=max"] +} + +# Production target with registry push +target "prod" { + inherits = ["app"] + output = ["type=registry"] +} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..77b9777 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,65 @@ +services: + # Frontend service + frontend: + build: + context: . + args: + # CouchDB Configuration + - VITE_COUCHDB_URL=${VITE_COUCHDB_URL:-http://couchdb:5984} + - VITE_COUCHDB_USER=${VITE_COUCHDB_USER:-admin} + - VITE_COUCHDB_PASSWORD=${VITE_COUCHDB_PASSWORD:-change-this-secure-password} + # Application Configuration + - APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080} + # OAuth Configuration (Optional) + - VITE_GOOGLE_CLIENT_ID=${VITE_GOOGLE_CLIENT_ID:-} + - VITE_GITHUB_CLIENT_ID=${VITE_GITHUB_CLIENT_ID:-} + # Build Environment + - NODE_ENV=${NODE_ENV:-production} + # Enable buildx for multi-platform builds + platforms: + - linux/amd64 + - linux/arm64 + ports: + - '8080:80' + depends_on: + couchdb: + condition: service_healthy + restart: unless-stopped + # Health check for the frontend container + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost/'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + labels: + - 'monitoring=true' + - 'service=frontend' + + # CouchDB service + couchdb: + image: couchdb:3.3.2 + volumes: + - ./couchdb-data:/opt/couchdb/data + environment: + - COUCHDB_USER=${COUCHDB_USER:-admin} + - COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-change-this-secure-password} + ports: + - '5984:5984' + restart: unless-stopped + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:5984/_up'] + interval: 30s + timeout: 10s + retries: 3 + labels: + - 'monitoring=true' + - 'service=couchdb' + + # Redis service (commented out as per requirements) + # redis: + # image: redis:alpine + # restart: unless-stopped + # labels: + # - "monitoring=true" + # - "service=redis" diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..ca40632 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Handle client-side routing + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} diff --git a/docs/DOCS_UPDATE_SUMMARY.md b/docs/DOCS_UPDATE_SUMMARY.md new file mode 100644 index 0000000..54a4329 --- /dev/null +++ b/docs/DOCS_UPDATE_SUMMARY.md @@ -0,0 +1,85 @@ +# Documentation Updates Summary + +## Files Updated + +### 📚 Main Documentation + +- **`docs/CODE_QUALITY.md`** - Comprehensive code quality and pre-commit hooks documentation +- **`README.md`** - Updated development and code quality sections +- **`SETUP_COMPLETE.md`** - Updated quick reference guide + +## Key Updates Made + +### 1. Enhanced Code Quality Documentation (`docs/CODE_QUALITY.md`) + +- ✅ Added detailed pre-commit hook descriptions +- ✅ Updated Python virtual environment paths for commands +- ✅ Added comprehensive troubleshooting section +- ✅ Enhanced IDE integration instructions with VS Code settings +- ✅ Added security tools documentation (detect-secrets) +- ✅ Updated manual command examples with correct paths + +### 2. Main README Updates (`README.md`) + +- ✅ Updated "Development Tools" section to include new formatting tools +- ✅ Enhanced "Code Quality" section with comprehensive commands +- ✅ Added reference to detailed code quality documentation +- ✅ Added Code Quality Guide to project documentation index +- ✅ Updated commands to reflect current npm scripts + +### 3. Quick Reference Guide (`SETUP_COMPLETE.md`) + +- ✅ Updated tool descriptions to be more comprehensive +- ✅ Added Python virtual environment information +- ✅ Updated command examples with correct paths +- ✅ Enhanced configuration file descriptions + +## Current Setup Summary + +### 🔧 Tools Configured + +- **Pre-commit hooks** with 15+ quality checks +- **Prettier** for comprehensive code formatting +- **ESLint** with TypeScript and React rules +- **TypeScript** type checking +- **Security scanning** with detect-secrets +- **Docker linting** with Hadolint +- **Shell script linting** with ShellCheck +- **Markdown linting** for documentation quality + +### 📁 Key Files + +- `.pre-commit-config.yaml` - Comprehensive hook configuration +- `.prettierrc` - Formatting rules optimized for TypeScript/React +- `eslint.config.cjs` - Enhanced linting rules +- `.editorconfig` - Editor consistency +- `.secrets.baseline` - Security baseline +- `scripts/setup-pre-commit.sh` - Automated setup +- Python virtual environment (`.venv/`) - Isolated tool environment + +### 🚀 Available Commands + +```bash +# Code Quality +bun run format # Format all files +bun run format:check # Check formatting +bun run lint # Lint code +bun run lint:fix # Fix linting issues +bun run type-check # TypeScript checks +bun run pre-commit # Run lint-staged + +# Pre-commit Hooks +/home/will/Code/meds/.venv/bin/pre-commit run --all-files +/home/will/Code/meds/.venv/bin/pre-commit autoupdate +``` + +## Next Steps + +1. **Test the setup**: Run `bun run format` and `bun run lint:fix` to verify everything works +2. **Make a commit**: Test that pre-commit hooks run automatically +3. **Configure IDE**: Install recommended VS Code extensions for optimal experience +4. **Review docs**: Check `docs/CODE_QUALITY.md` for comprehensive setup details + +--- + +**All documentation is now up-to-date with the current code quality setup! 🎉** diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..20d0cd0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,81 @@ +# 📚 Documenta#### 💻 Development + +- **[API Documentation](development/API.md)** - REST API endpoints and usage +- **[Code Quality](development/CODE_QUALITY.md)** - Linting, formatting, and quality standards +- **[Application Security](development/APPLICATION_SECURITY.md)** - Application security practices +- **[Security Changes](development/SECURITY_CHANGES.md)** - Recent security updates and changesndex + +Welcome to the RxMinder documentation! This guide will help you navigate through all available documentation organized by category. + +## 🏗️ Architecture & Design + +- **[Project Structure](architecture/PROJECT_STRUCTURE.md)** - Complete overview of the codebase organization +- **[Template Approach](architecture/TEMPLATE_APPROACH.md)** - Design philosophy and template methodology + +## 🚀 Setup & Configuration + +- **[Complete Template Configuration](setup/COMPLETE_TEMPLATE_CONFIGURATION.md)** - Full setup guide +- **[Setup Complete](setup/SETUP_COMPLETE.md)** - Post-setup verification checklist + +## 💻 Development + +- **[API Documentation](development/API.md)** - REST API endpoints and usage +- **[Code Quality](development/CODE_QUALITY.md)** - Linting, formatting, and quality standards +- **[Security](development/SECURITY.md)** - Security guidelines and best practices +- **[Security Changes](development/SECURITY_CHANGES.md)** - Recent security updates and changes + +## 🚢 Deployment + +- **[Deployment Guide](deployment/DEPLOYMENT.md)** - General deployment instructions +- **[Docker Configuration](deployment/DOCKER_IMAGE_CONFIGURATION.md)** - Docker setup and configuration +- **[Gitea Setup](deployment/GITEA_SETUP.md)** - Gitea CI/CD configuration +- **[Storage Configuration](deployment/STORAGE_CONFIGURATION.md)** - Database and storage setup + +## 🔄 Migration Guides + +- **[NodeJS Pre-commit Migration](migration/NODEJS_PRECOMMIT_MIGRATION.md)** - Migration from Python to NodeJS pre-commit hooks +- **[Buildx Migration](migration/BUILDX_MIGRATION.md)** - Docker Buildx migration guide + +## 📝 Project Information + +- **[README](../README.md)** - Main project overview and quick start +- **[Contributing](../CONTRIBUTING.md)** - How to contribute to the project +- **[Changelog](../CHANGELOG.md)** - Version history and changes +- **[License](../LICENSE)** - Project license information + +## 📋 Documentation Meta + +- **[Documentation Reorganization](REORGANIZATION_SUMMARY.md)** - How we restructured the docs +- **[Docs Update Summary](DOCS_UPDATE_SUMMARY.md)** - Legacy documentation summary + +## 🔍 Quick Navigation + +### For New Developers + +1. Start with [README](../README.md) +2. Review [Project Structure](architecture/PROJECT_STRUCTURE.md) +3. Follow [Complete Template Configuration](setup/COMPLETE_TEMPLATE_CONFIGURATION.md) +4. Read [Code Quality](development/CODE_QUALITY.md) guidelines + +### For Deployment + +1. Read [Deployment Guide](deployment/DEPLOYMENT.md) +2. Configure [Docker](deployment/DOCKER_IMAGE_CONFIGURATION.md) +3. Set up [Storage](deployment/STORAGE_CONFIGURATION.md) +4. Review [Security](development/SECURITY.md) requirements + +### For API Integration + +1. Check [API Documentation](development/API.md) +2. Review [Security](development/SECURITY.md) requirements + +### For Migration Tasks + +1. [NodeJS Pre-commit Migration](migration/NODEJS_PRECOMMIT_MIGRATION.md) - For modernizing git hooks +2. [Buildx Migration](migration/BUILDX_MIGRATION.md) - For Docker build improvements + +--- + +📋 **Last Updated:** September 6, 2025 +🔄 **Documentation Version:** 2.0 +📦 **Project Version:** 0.0.0 diff --git a/docs/REORGANIZATION_SUMMARY.md b/docs/REORGANIZATION_SUMMARY.md new file mode 100644 index 0000000..ec5277b --- /dev/null +++ b/docs/REORGANIZATION_SUMMARY.md @@ -0,0 +1,136 @@ +# 📚 Documentation Reorganization Summary + +## Overview + +Successfully reorganized the project documentation from scattered root-level files into a structured, categorized system for better navigation and maintenance. + +## Changes Made + +### 🗂️ New Structure Created + +``` +docs/ +├── README.md # 📋 Main documentation index +├── DOCS_UPDATE_SUMMARY.md # 📝 Legacy docs summary +├── architecture/ # 🏗️ Design & Architecture +│ ├── PROJECT_STRUCTURE.md +│ └── TEMPLATE_APPROACH.md +├── setup/ # 🚀 Setup & Configuration +│ ├── COMPLETE_TEMPLATE_CONFIGURATION.md +│ └── SETUP_COMPLETE.md +├── development/ # 💻 Development Guides +│ ├── API.md +│ ├── CODE_QUALITY.md +│ ├── SECURITY.md +│ └── SECURITY_CHANGES.md +├── deployment/ # 🚢 Deployment Guides +│ ├── DEPLOYMENT.md +│ ├── DOCKER_IMAGE_CONFIGURATION.md +│ ├── GITEA_SETUP.md +│ └── STORAGE_CONFIGURATION.md +└── migration/ # 🔄 Migration Guides + ├── BUILDX_MIGRATION.md + └── NODEJS_PRECOMMIT_MIGRATION.md +``` + +### 📁 Files Moved + +#### From Root → `docs/architecture/` + +- `PROJECT_STRUCTURE.md` +- `TEMPLATE_APPROACH.md` + +#### From Root → `docs/setup/` + +- `COMPLETE_TEMPLATE_CONFIGURATION.md` +- `SETUP_COMPLETE.md` + +#### From `docs/` → `docs/development/` + +- `API.md` +- `CODE_QUALITY.md` +- `SECURITY.md` +- `SECURITY_CHANGES.md` (from root) + +#### From `docs/` → `docs/deployment/` + +- `DEPLOYMENT.md` +- `DOCKER_IMAGE_CONFIGURATION.md` (from root) +- `GITEA_SETUP.md` (from root) +- `STORAGE_CONFIGURATION.md` (from root) + +#### From Root → `docs/migration/` + +- `BUILDX_MIGRATION.md` +- `NODEJS_PRECOMMIT_MIGRATION.md` + +#### To `docs/` root + +- `DOCS_UPDATE_SUMMARY.md` (from root) + +### 📋 New Documentation Index + +Created `docs/README.md` with: + +- **Complete categorized index** of all documentation +- **Quick navigation paths** for different user types +- **Direct links** to all organized documents +- **Usage scenarios** (new developers, deployment, API integration, etc.) + +### 🔗 Updated References + +- Updated main `README.md` to include comprehensive documentation section +- Fixed broken link to `CODE_QUALITY.md` in main README +- Added structured documentation navigation + +## Benefits + +### 🎯 **Improved Organization** + +- **Logical categorization** by purpose and audience +- **Easier navigation** with clear folder structure +- **Reduced root directory clutter** + +### 👥 **Better User Experience** + +- **Role-based navigation** (developers, ops, admins) +- **Quick-start paths** for different scenarios +- **Comprehensive index** for easy discovery + +### 🔧 **Maintainability** + +- **Centralized documentation management** +- **Clear ownership** by category +- **Easier updates** and maintenance + +### 📈 **Scalability** + +- **Room for growth** in each category +- **Consistent structure** for new docs +- **Template for future organization** + +## Navigation Guide + +### 🔰 For New Team Members + +1. Start with main [`README.md`](../README.md) +2. Visit [`docs/README.md`](README.md) for complete index +3. Follow role-specific quick navigation paths + +### 📝 For Contributors + +1. Check [`docs/development/`](development/) for coding standards +2. Review [`docs/architecture/`](architecture/) for design context +3. Follow [`CONTRIBUTING.md`](../CONTRIBUTING.md) guidelines + +### 🚀 For Deployment + +1. Start with [`docs/deployment/DEPLOYMENT.md`](deployment/DEPLOYMENT.md) +2. Follow specific deployment guides in [`docs/deployment/`](deployment/) +3. Check [`docs/setup/`](setup/) for configuration help + +--- + +**Documentation Structure Version:** 2.0 +**Reorganized:** September 6, 2025 +**Status:** ✅ Complete diff --git a/docs/architecture/PROJECT_STRUCTURE.md b/docs/architecture/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..8b2793b --- /dev/null +++ b/docs/architecture/PROJECT_STRUCTURE.md @@ -0,0 +1,180 @@ +# 📁 Project Structure + +## Final Organized Structure + +``` +rxminder/ +├── 📄 README.md # Main documentation +├── package.json # Dependencies and scripts +├── ⚙️ vite.config.ts # Build configuration +├── 📝 tsconfig.json # TypeScript configuration +├── 🎨 index.html # Entry point +├── 🔒 .env.example # Environment template +├── 📊 metadata.json # Project metadata +├── 🖼️ banner.jpeg # Project banner image +│ +├── � docker/ # Container configuration +│ ├── 🐳 Dockerfile # Multi-stage Docker build +│ ├── � docker-compose.yaml # Service orchestration +│ ├── 🌐 nginx.conf # Production web server config +│ └── 🚫 .dockerignore # Docker ignore patterns +│ +├── 📁 scripts/ # All deployment and utility scripts +│ ├── 🚀 deploy.sh # Production deployment +│ ├── ⚡ deploy-k8s.sh # Kubernetes deployment +│ ├── 🔧 setup.sh # Development setup +│ ├── 🌱 seed-production.js # Database seeding +│ ├── ✅ validate-env.sh # Environment validation +│ └── 🧪 validate-deployment.sh # Deployment testing +│ +├── 📁 tests/ # Testing infrastructure +│ ├── 📝 README.md # Testing documentation +│ ├── ⚙️ setup.ts # Jest configuration +│ ├── 📁 integration/ # Integration tests +│ │ └── 🧪 production.test.js # Production validation +│ ├── 📁 manual/ # Manual testing scripts +│ │ ├── 🔧 admin-login-debug.js # Admin debugging +│ │ ├── 🔧 auth-db-debug.js # Auth debugging +│ │ └── 🔧 debug-email-validation.js # Email debugging +│ └── 📁 e2e/ # End-to-end tests with Playwright +│ ├── 📝 README.md # E2E testing documentation +│ ├── 🧪 fixtures.ts # Custom test fixtures +│ ├── 🧪 helpers.ts # Test utilities and data +│ ├── 🧪 auth.spec.ts # Authentication flow tests +│ ├── 🧪 medication.spec.ts # Medication management tests +│ ├── 🧪 admin.spec.ts # Admin interface tests +│ ├── 🧪 ui-navigation.spec.ts # UI and navigation tests +│ └── 🧪 reminders.spec.ts # Reminder system tests +│ +├── 📁 components/ # React components (organized by feature) +│ ├── 📝 README.md # Component architecture docs +│ ├── 📁 medication/ # Medication-related components +│ │ ├── 💊 AddMedicationModal.tsx +│ │ ├── ✏️ EditMedicationModal.tsx +│ │ ├── 📋 ManageMedicationsModal.tsx +│ │ ├── 🏷️ DoseCard.tsx +│ │ └── 📦 index.ts # Feature exports +│ ├── 📁 auth/ # Authentication components +│ │ ├── 🔐 AuthPage.tsx # Login/register interface +│ │ ├── 👤 AvatarDropdown.tsx # User menu +│ │ ├── 🔑 ChangePasswordModal.tsx +│ │ └── 📦 index.ts # Feature exports +│ ├── 📁 admin/ # Admin interface components +│ │ ├── 👑 AdminInterface.tsx # User management +│ │ └── 📦 index.ts # Feature exports +│ ├── 📁 modals/ # Modal components +│ │ ├── ⚙️ AccountModal.tsx # User settings +│ │ ├── ➕ AddReminderModal.tsx # Add reminders +│ │ ├── ✏️ EditReminderModal.tsx +│ │ ├── 📚 HistoryModal.tsx # Medication history +│ │ ├── 📋 ManageRemindersModal.tsx +│ │ ├── 🎯 OnboardingModal.tsx # New user setup +│ │ ├── 📊 StatsModal.tsx # Analytics dashboard +│ │ └── 📦 index.ts # Feature exports +│ ├── 📁 ui/ # Reusable UI components +│ │ ├── 📊 BarChart.tsx # Data visualization +│ │ ├── 🔔 ReminderCard.tsx # Reminder display +│ │ ├── 🎨 ThemeSwitcher.tsx # Dark/light theme +│ │ └── 📦 index.ts # Feature exports +│ └── 📁 icons/ # Icon components +│ └── 🎨 Icons.tsx # All icon definitions +│ +├── 📁 services/ # Business logic & APIs +│ ├── 🗄️ couchdb.ts # Mock database service +│ ├── 🗄️ couchdb.production.ts # Real CouchDB service +│ ├── 🏭 couchdb.factory.ts # Service factory +│ ├── 📧 email.ts # Email utilities +│ ├── 📧 mailgun.service.ts # Email delivery +│ ├── 📧 mailgun.config.ts # Email configuration +│ ├── 🌱 database.seeder.ts # Data seeding +│ ├── 🔐 oauth.ts # OAuth integration +│ └── 📁 auth/ # Authentication services +│ ├── 🔐 auth.service.ts # Core auth logic +│ ├── 🔐 auth.types.ts # Auth type definitions +│ ├── 🔐 auth.constants.ts # Auth constants +│ ├── 🔐 auth.error.ts # Error handling +│ ├── 🔐 auth.middleware.ts # Middleware +│ ├── ✉️ emailVerification.service.ts +│ ├── 📁 templates/ # Email templates +│ │ └── ✉️ verification.email.ts +│ └── 📁 __tests__/ # Unit tests +│ ├── 🧪 auth.integration.test.ts +│ └── 🧪 emailVerification.test.ts +│ +├── 📁 contexts/ # React context providers +│ └── 👤 UserContext.tsx # User state management +│ +├── 📁 hooks/ # Custom React hooks +│ ├── 💾 useLocalStorage.ts # Persistent storage +│ ├── ⚙️ useSettings.ts # User preferences +│ ├── 🎨 useTheme.ts # Theme management +│ └── 👤 useUserData.ts # User data management +│ +├── 📁 utils/ # Utility functions +│ └── ⏰ schedule.ts # Reminder scheduling +│ +├── 📁 docs/ # Project documentation +│ ├── 🔐 SECURITY.md # Security guidelines +│ ├── 🚀 DEPLOYMENT.md # Deployment instructions +│ └── 📖 API.md # API documentation +│ +├── 📁 k8s/ # Kubernetes manifests +│ ├── 📝 README.md # K8s deployment guide +│ ├── 🗺️ configmap.yaml # Configuration +│ ├── 🔒 *-secret.yaml # Secrets +│ ├── 🚀 *-deployment.yaml # Deployments +│ ├── 🌐 *-service.yaml # Services +│ ├── 📊 hpa.yaml # Auto-scaling +│ ├── 🌐 ingress.yaml # Load balancing +│ └── 🔒 network-policy.yaml # Network security +│ +└── 📁 .github/ # GitHub configuration + ├── 📝 pull_request_template.md + └── 📁 ISSUE_TEMPLATE/ + ├── 🐛 bug_report.md + └── ✨ feature_request.md +``` + +## Key Organizational Principles + +### ✅ **Feature-Based Organization** + +- Components grouped by functionality (medication, auth, admin, etc.) +- Clear separation of concerns +- Easy to locate related files + +### ✅ **Script Centralization** + +- All deployment and utility scripts in `/scripts/` +- Consistent naming conventions +- Easy access via npm/bun scripts + +### ✅ **Testing Structure** + +- Unit tests alongside source code (`services/auth/__tests__/`) +- Integration tests in `/tests/integration/` +- E2E tests with Playwright in `/tests/e2e/` +- Manual debugging tools in `/tests/manual/` +- Comprehensive test documentation +- TypeScript support with temporary type declarations + +### ✅ **Documentation Organization** + +- Feature-specific READMEs in relevant folders +- Centralized docs in `/docs/` folder +- Clear architectural documentation + +### ✅ **Configuration Management** + +- Environment files at root level +- Build configurations easily accessible +- Docker and K8s configs clearly separated + +## Benefits + +🎯 **Maintainability** - Clear structure makes code easy to maintain +🔍 **Discoverability** - Logical organization helps find files quickly +🧪 **Testability** - Well-organized test structure +📦 **Deployability** - Scripts and configs clearly separated +👥 **Team Collaboration** - Consistent patterns across the project +📈 **Scalability** - Structure supports growth and new features diff --git a/docs/architecture/TEMPLATE_APPROACH.md b/docs/architecture/TEMPLATE_APPROACH.md new file mode 100644 index 0000000..bd45c86 --- /dev/null +++ b/docs/architecture/TEMPLATE_APPROACH.md @@ -0,0 +1,159 @@ +# 🎯 Template-Based Kubernetes Configuration + +## Overview + +We've implemented a **template-based approach** using environment variables instead of manual base64 encoding for Kubernetes secrets. This is much more user-friendly and secure. + +## 🆚 Before vs After Comparison + +### ❌ Before (Manual Base64 Encoding) + +**Old approach required manual base64 encoding:** + +```yaml +# k8s/couchdb-secret.yaml +apiVersion: v1 +kind: Secret +data: + # User had to manually encode: + # echo -n "admin" | base64 -> YWRtaW4= + # echo -n "password" | base64 -> cGFzc3dvcmQ= + username: YWRtaW4= + password: cGFzc3dvcmQ= +``` + +**Problems:** + +- 😣 Manual base64 encoding required +- 🔧 Error-prone (encoding mistakes) +- 📝 Hard to read/verify credentials +- 🔒 Credentials visible in YAML files + +### ✅ After (Template-Based) + +**New approach uses templates with automatic substitution:** + +```yaml +# k8s/couchdb-secret.yaml.template +apiVersion: v1 +kind: Secret +metadata: + name: couchdb-secret + labels: + app: ${APP_NAME} +type: Opaque +stringData: + # Kubernetes automatically base64 encodes stringData + username: ${COUCHDB_USER} + password: ${COUCHDB_PASSWORD} +``` + +**Benefits:** + +- ✅ No manual base64 encoding needed +- ✅ Environment variables from `.env` file +- ✅ Human-readable configuration +- ✅ Automatic deployment script +- ✅ Customizable app names + +## 🚀 How It Works + +### 1. Configuration in `.env` + +```bash +# .env (user-friendly configuration) +APP_NAME=my-rxminder +COUCHDB_USER=admin +COUCHDB_PASSWORD=super-secure-password-123 +INGRESS_HOST=rxminder.mydomain.com +``` + +### 2. Template Substitution + +```bash +# Automatic substitution with envsubst +envsubst < k8s/couchdb-secret.yaml.template +``` + +**Result:** + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: couchdb-secret + labels: + app: my-rxminder +type: Opaque +stringData: + username: admin + password: super-secure-password-123 +``` + +### 3. Kubernetes Processing + +- Kubernetes automatically base64 encodes `stringData` fields +- No manual encoding required +- More secure and reliable + +## 🎛️ Deployment Options + +### Option 1: Automated Script (Recommended) + +```bash +# Copy and configure +cp .env.example .env +nano .env + +# Deploy everything +./scripts/k8s-deploy-template.sh deploy +``` + +### Option 2: Manual Template Processing + +```bash +# Set environment variables +export APP_NAME=my-rxminder +export COUCHDB_PASSWORD=secure-password + +# Process templates +envsubst < k8s/couchdb-secret.yaml.template | kubectl apply -f - +envsubst < k8s/ingress.yaml.template | kubectl apply -f - +``` + +## 🔧 Template Files Created + +1. **`k8s/couchdb-secret.yaml.template`** - Database credentials +2. **`k8s/ingress.yaml.template`** - Ingress with custom hostname +3. **`k8s/configmap.yaml.template`** - Application configuration +4. **`k8s/frontend-deployment.yaml.template`** - Frontend deployment +5. **`scripts/k8s-deploy-template.sh`** - Automated deployment script + +## 🛡️ Security Benefits + +- **No hardcoded credentials** in version control +- **Environment-specific configuration** via `.env` files +- **Automatic validation** of required variables +- **Kubernetes stringData** (auto base64 encoding) +- **Clear separation** of config and code + +## 📝 User Experience Improvements + +| Aspect | Before | After | +| -------------------- | ------------------------ | ---------------------- | +| **Setup Complexity** | High (manual base64) | Low (edit .env) | +| **Error Rate** | High (encoding mistakes) | Low (plain text) | +| **Readability** | Poor (base64 strings) | Excellent (plain text) | +| **Customization** | Manual file editing | Environment variables | +| **Deployment** | Multi-step manual | Single command | + +## 🎯 Result + +The template-based approach makes RxMinder deployment: + +- **More user-friendly** - No technical encoding required +- **More secure** - Credentials externalized to `.env` +- **More maintainable** - Clear separation of config and manifests +- **More flexible** - Easy customization via environment variables + +This is a **production-ready, enterprise-grade** configuration management approach that follows Kubernetes best practices. diff --git a/docs/deployment/DEPLOYMENT.md b/docs/deployment/DEPLOYMENT.md new file mode 100644 index 0000000..63d5463 --- /dev/null +++ b/docs/deployment/DEPLOYMENT.md @@ -0,0 +1,538 @@ +# Deployment Guide + +## 🚀 Complete Deployment Guide for Medication Reminder App + +### **Prerequisites** + +#### **System Requirements** + +- Docker 20.10+ and Docker Compose 2.0+ +- 2GB RAM minimum, 4GB recommended +- 10GB disk space for application and data +- Linux/macOS/Windows with WSL2 + +#### **Required Accounts** + +- [Mailgun Account](https://mailgun.com) for email services +- Domain name for production deployment (optional) +- SSL certificate for HTTPS (recommended) + +### **Environment Setup** + +#### **1. Clone Repository** + +```bash +git clone +cd meds +``` + +#### **2. Configure Environment** + +```bash +# Copy template +cp .env.example .env + +# Edit with your credentials +nano .env +``` + +**Required Variables:** + +```bash +# Application Configuration +APP_BASE_URL=https://yourdomain.com + +# CouchDB Configuration +COUCHDB_USER=admin +COUCHDB_PASSWORD=super-secure-password-123! +VITE_COUCHDB_URL=http://couchdb:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=super-secure-password-123! + +# Mailgun Configuration +MAILGUN_API_KEY=key-1234567890abcdef1234567890abcdef +MAILGUN_DOMAIN=mg.yourdomain.com +MAILGUN_FROM_EMAIL=noreply@yourdomain.com + +# Production Settings +NODE_ENV=production +``` + +### **Local Development Deployment** + +#### **Quick Start** + +```bash +# Automated setup +./setup.sh + +# Manual setup +bun install +docker compose up -d +bun run seed-production.js +``` + +#### **Development URLs** + +- Frontend: http://localhost:8080 +- CouchDB: http://localhost:5984 +- Admin Panel: http://localhost:5984/\_utils + +### **Production Deployment** + +#### **Method 1: Automated Script** + +```bash +# Secure deployment with validation +./deploy.sh production +``` + +#### **Method 2: Manual Docker Compose** + +```bash +# Build images +docker compose build --no-cache + +# Start services +docker compose up -d + +# Seed database +node seed-production.js + +# Verify deployment +bun test-production.js +``` + +#### **Method 3: Docker Swarm** + +```bash +# Initialize swarm +docker swarm init + +# Deploy stack +docker stack deploy -c docker/docker-compose.yaml meds-stack + +# Scale services +docker service scale meds-stack_frontend=3 +``` + +### **Cloud Platform Deployments** + +#### **AWS EC2 Deployment** + +**1. Launch EC2 Instance** + +```bash +# Amazon Linux 2 AMI +# Instance type: t3.medium or larger +# Security group: Allow ports 22, 80, 443, 8080 +``` + +**2. Install Dependencies** + +```bash +# Connect to instance +ssh -i your-key.pem ec2-user@your-instance-ip + +# Install Docker +sudo yum update -y +sudo yum install -y docker +sudo service docker start +sudo usermod -a -G docker ec2-user + +# Install Docker Compose +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose +``` + +**3. Deploy Application** + +```bash +# Clone and configure +git clone +cd meds +cp .env.example .env +# Edit .env with production values + +# Deploy +./deploy.sh production +``` + +#### **Google Cloud Platform Deployment** + +**1. Cloud Run Deployment** + +```bash +# Build and push image +gcloud builds submit --tag gcr.io/PROJECT-ID/meds-app + +# Deploy service +gcloud run deploy meds-app \ + --image gcr.io/PROJECT-ID/meds-app \ + --platform managed \ + --region us-central1 \ + --set-env-vars COUCHDB_URL=your-couchdb-url \ + --set-env-vars MAILGUN_API_KEY=your-key \ + --allow-unauthenticated +``` + +**2. Compute Engine Deployment** + +```bash +# Create instance +gcloud compute instances create meds-server \ + --image-family debian-11 \ + --image-project debian-cloud \ + --machine-type e2-medium \ + --tags http-server,https-server + +# SSH and install +gcloud compute ssh meds-server +# Follow standard installation steps +``` + +#### **Digital Ocean Deployment** + +**1. Droplet Setup** + +```bash +# Create droplet with Docker pre-installed +# Or install Docker manually on Ubuntu droplet + +# Connect and deploy +ssh root@your-droplet-ip +git clone +cd meds +./setup.sh +./deploy.sh production +``` + +**2. App Platform Deployment** + +```bash +# Create app.yaml +version: 1 +services: +- name: meds-app + source_dir: / + github: + repo: your-username/meds + branch: main + build_command: bun run build + environment_slug: node-js + instance_count: 1 + instance_size_slug: basic-xxs + envs: + - key: COUCHDB_URL + value: ${COUCHDB_URL} + - key: MAILGUN_API_KEY + value: ${MAILGUN_API_KEY} + +# Deploy +doctl apps create --spec app.yaml +``` + +### **Kubernetes Deployment** + +#### **Method 1: Automated Deployment Script (Recommended)** + +```bash +# Configure environment +cp .env.example .env +# Edit .env with your settings: +# INGRESS_HOST=app.meds.192.168.1.100.nip.io # For local cluster +# INGRESS_HOST=meds.yourdomain.com # For production + +# Deploy with environment substitution +./deploy-k8s.sh + +# Check deployment status +./deploy-k8s.sh --status + +# Deploy with custom environment file +./deploy-k8s.sh --env .env.production + +# Preview deployment (dry run) +./deploy-k8s.sh --dry-run +``` + +#### **Method 2: Manual Deployment** + +#### **1. Create Namespace and Secrets** + +```bash +# Create namespace +kubectl create namespace meds-app + +# Create secrets +kubectl create secret generic meds-secrets \ + --from-literal=couchdb-user=admin \ + --from-literal=couchdb-password=secure-password \ + --from-literal=mailgun-api-key=your-api-key \ + --namespace meds-app +``` + +#### **2. Deploy Services** + +```bash +# Apply Kubernetes manifests +kubectl apply -f k8s/ --namespace meds-app + +# Check deployment status +kubectl get pods -n meds-app +kubectl get services -n meds-app +``` + +#### **3. Configure Ingress (Manual)** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: meds-ingress + namespace: meds-app + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + tls: + - hosts: + - meds.yourdomain.com + secretName: meds-tls + rules: + - host: meds.yourdomain.com # Update this to your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: meds-frontend + port: + number: 80 +``` + +### **SSL/HTTPS Configuration** + +#### **Let's Encrypt with Nginx** + +```bash +# Install certbot +sudo apt-get install certbot python3-certbot-nginx + +# Get certificate +sudo certbot --nginx -d yourdomain.com + +# Auto-renewal +sudo crontab -e +# Add: 0 12 * * * /usr/bin/certbot renew --quiet +``` + +#### **Cloudflare SSL** + +```bash +# Update docker/nginx.conf for Cloudflare +# Set ssl_certificate and ssl_certificate_key +# Configure Cloudflare for Full (Strict) SSL +``` + +### **Database Backup and Recovery** + +#### **CouchDB Backup** + +```bash +# Create backup script +#!/bin/bash +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="/backup/couchdb" + +# Backup all databases +curl -X GET http://admin:password@localhost:5984/_all_dbs | \ +jq -r '.[]' | while read db; do + curl -X GET "http://admin:password@localhost:5984/$db/_all_docs?include_docs=true" \ + > "$BACKUP_DIR/${db}_${DATE}.json" +done +``` + +#### **Automated Backups** + +```bash +# Add to crontab +0 2 * * * /opt/meds/backup-couchdb.sh + +# Upload to cloud storage +aws s3 cp /backup/couchdb/ s3://your-backup-bucket/ --recursive +``` + +### **Monitoring and Logging** + +#### **Health Checks** + +```bash +# Application health +curl -f http://localhost:8080/health + +# CouchDB health +curl -f http://admin:password@localhost:5984/_up + +# Docker container health +docker compose ps +``` + +#### **Log Management** + +```bash +# View logs +docker compose logs -f frontend +docker compose logs -f couchdb + +# Log rotation +# Configure in docker/docker-compose.yaml: +logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +``` + +#### **Performance Monitoring** + +```bash +# Resource usage +docker stats + +# Application metrics +# Implement custom metrics endpoint +# Use Prometheus/Grafana for monitoring +``` + +### **Scaling and Load Balancing** + +#### **Horizontal Scaling** + +```bash +# Scale frontend containers +docker compose up -d --scale frontend=3 + +# Load balancer configuration +# Use nginx, HAProxy, or cloud load balancer +``` + +#### **Database Scaling** + +```bash +# CouchDB clustering +# Configure multiple CouchDB nodes +# Set up replication between nodes +``` + +### **Security Hardening** + +#### **Firewall Configuration** + +```bash +# UFW (Ubuntu) +sudo ufw allow 22/tcp +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw deny 5984/tcp # CouchDB admin (internal only) +sudo ufw enable +``` + +#### **Container Security** + +```bash +# Run security scan +docker scout cves meds-frontend:latest + +# Update base images regularly +docker compose build --no-cache +``` + +### **Troubleshooting** + +#### **Common Issues** + +**1. Environment Variables Not Loading** + +```bash +# Check file format +cat -A .env + +# Verify Docker Compose config +docker compose config +``` + +**2. Database Connection Issues** + +```bash +# Test CouchDB connection +curl -u admin:password http://localhost:5984/ + +# Check container logs +docker compose logs couchdb +``` + +**3. Email Not Sending** + +```bash +# Verify Mailgun configuration +curl -s --user 'api:YOUR_API_KEY' \ + https://api.mailgun.net/v3/YOUR_DOMAIN/messages \ + -F from='test@YOUR_DOMAIN' \ + -F to='you@example.com' \ + -F subject='Test' \ + -F text='Testing' +``` + +**4. Frontend Build Failures** + +```bash +# Clear cache and rebuild +docker compose build --no-cache frontend +``` + +### **Maintenance** + +#### **Regular Tasks** + +- Update dependencies monthly +- Rotate credentials quarterly +- Backup database daily +- Monitor disk space weekly +- Review security logs daily + +#### **Update Process** + +```bash +# 1. Backup current deployment +./backup.sh + +# 2. Pull latest changes +git pull origin main + +# 3. Update dependencies +bun install + +# 4. Rebuild and deploy +docker compose build --no-cache +docker compose up -d + +# 5. Verify deployment +bun test-production.js +``` + +### **Support and Documentation** + +#### **Getting Help** + +- GitHub Issues: Create issue for bugs/features +- Documentation: Check README.md and docs/ +- Community: Join our Discord/Slack channel + +#### **Professional Support** + +- Enterprise support available +- Custom deployment assistance +- Security auditing services +- Performance optimization consulting diff --git a/docs/deployment/DOCKER_IMAGE_CONFIGURATION.md b/docs/deployment/DOCKER_IMAGE_CONFIGURATION.md new file mode 100644 index 0000000..085ba47 --- /dev/null +++ b/docs/deployment/DOCKER_IMAGE_CONFIGURATION.md @@ -0,0 +1,265 @@ +# 🐳 Docker Image Configuration + +## Overview + +RxMinder now supports configurable Docker images via environment variables, enabling flexible deployment across different registries, environments, and versions. + +## 🎯 Docker Image Variable + +### **DOCKER_IMAGE** + +The complete Docker image specification including registry, repository, and tag. + +**Format:** `[registry/]repository:tag` + +## 🌐 Registry Examples + +### Public Registries + +#### Docker Hub + +```bash +# Official image on Docker Hub +DOCKER_IMAGE=rxminder/rxminder:latest +DOCKER_IMAGE=rxminder/rxminder:v1.2.0 +DOCKER_IMAGE=rxminder/rxminder:stable +``` + +#### GitHub Container Registry (ghcr.io) + +```bash +# GitHub Packages +DOCKER_IMAGE=ghcr.io/username/rxminder:latest +DOCKER_IMAGE=ghcr.io/organization/rxminder:v1.2.0 +DOCKER_IMAGE=ghcr.io/username/rxminder:dev-branch +``` + +#### GitLab Container Registry + +```bash +# GitLab Registry +DOCKER_IMAGE=registry.gitlab.com/username/rxminder:latest +DOCKER_IMAGE=registry.gitlab.com/group/rxminder:production +``` + +### Private/Self-Hosted Registries + +#### Gitea Registry + +```bash +# Current default (Gitea) +DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:latest +DOCKER_IMAGE=gitea-http.taildb3494.ts.net/will/meds:v1.2.0 +``` + +#### Harbor Registry + +```bash +# Harbor enterprise registry +DOCKER_IMAGE=harbor.company.com/rxminder/rxminder:latest +DOCKER_IMAGE=harbor.company.com/rxminder/rxminder:production +``` + +#### Local Registry + +```bash +# Local development registry +DOCKER_IMAGE=localhost:5000/rxminder:latest +DOCKER_IMAGE=registry.local:5000/rxminder:dev +``` + +### Cloud Provider Registries + +#### AWS Elastic Container Registry (ECR) + +```bash +# AWS ECR +DOCKER_IMAGE=123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:latest +DOCKER_IMAGE=123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.2.0 +``` + +#### Google Container Registry (GCR) + +```bash +# Google Cloud Registry +DOCKER_IMAGE=gcr.io/project-id/rxminder:latest +DOCKER_IMAGE=us.gcr.io/project-id/rxminder:production +``` + +#### Azure Container Registry (ACR) + +```bash +# Azure Container Registry +DOCKER_IMAGE=myregistry.azurecr.io/rxminder:latest +DOCKER_IMAGE=myregistry.azurecr.io/rxminder:stable +``` + +## 🏷️ Tagging Strategies + +### Environment-Based Tagging + +```bash +# Development +DOCKER_IMAGE=myregistry.com/rxminder:dev +DOCKER_IMAGE=myregistry.com/rxminder:develop-20250906 + +# Staging +DOCKER_IMAGE=myregistry.com/rxminder:staging +DOCKER_IMAGE=myregistry.com/rxminder:release-candidate + +# Production +DOCKER_IMAGE=myregistry.com/rxminder:stable +DOCKER_IMAGE=myregistry.com/rxminder:v1.2.0 +``` + +### Git-Based Tagging + +```bash +# Branch-based +DOCKER_IMAGE=myregistry.com/rxminder:main +DOCKER_IMAGE=myregistry.com/rxminder:feature-auth + +# Commit-based +DOCKER_IMAGE=myregistry.com/rxminder:sha-abc1234 +DOCKER_IMAGE=myregistry.com/rxminder:pr-123 +``` + +### Semantic Versioning + +```bash +# Semantic versions +DOCKER_IMAGE=myregistry.com/rxminder:v1.0.0 +DOCKER_IMAGE=myregistry.com/rxminder:v1.2.3-beta +DOCKER_IMAGE=myregistry.com/rxminder:v2.0.0-rc1 +``` + +## 🎪 Environment-Specific Configurations + +### Development (.env) + +```bash +APP_NAME=rxminder-dev +DOCKER_IMAGE=localhost:5000/rxminder:dev +STORAGE_CLASS=local-path +STORAGE_SIZE=5Gi +INGRESS_HOST=rxminder-dev.local +``` + +### Staging (.env.staging) + +```bash +APP_NAME=rxminder-staging +DOCKER_IMAGE=myregistry.com/rxminder:staging +STORAGE_CLASS=longhorn +STORAGE_SIZE=10Gi +INGRESS_HOST=staging.rxminder.company.com +``` + +### Production (.env.production) + +```bash +APP_NAME=rxminder +DOCKER_IMAGE=myregistry.com/rxminder:v1.2.0 # Fixed version for stability +STORAGE_CLASS=fast-ssd +STORAGE_SIZE=50Gi +INGRESS_HOST=rxminder.company.com +``` + +## 🚀 CI/CD Integration + +### GitHub Actions Example + +```yaml +# .github/workflows/deploy.yml +- name: Deploy to Kubernetes + env: + DOCKER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }} + run: | + echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> .env + ./scripts/k8s-deploy-template.sh deploy +``` + +### GitLab CI Example + +```yaml +# .gitlab-ci.yml +deploy: + variables: + DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + script: + - echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> .env + - ./scripts/k8s-deploy-template.sh deploy +``` + +## 🔒 Registry Authentication + +### Docker Registry Secrets + +```bash +# Create registry secret for private registries +kubectl create secret docker-registry regcred \ + --docker-server=myregistry.com \ + --docker-username=username \ + --docker-password=password \ + --docker-email=email@company.com + +# Update deployment to use the secret +# (Add imagePullSecrets to deployment template if needed) +``` + +### Cloud Provider Authentication + +```bash +# AWS ECR +aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com + +# Google GCR +gcloud auth configure-docker + +# Azure ACR +az acr login --name myregistry +``` + +## 💡 Best Practices + +### Production Recommendations + +- ✅ **Use specific tags** (not `:latest`) for production +- ✅ **Pin to exact versions** for stability +- ✅ **Use semantic versioning** for releases +- ✅ **Separate registries** for different environments +- ✅ **Enable vulnerability scanning** on registries + +### Development Workflow + +- ✅ **Use `:dev` or `:latest`** for development +- ✅ **Branch-based tags** for feature development +- ✅ **Local registries** for fast iteration +- ✅ **Automated builds** on code changes + +### Security Considerations + +- ✅ **Private registries** for proprietary code +- ✅ **Registry authentication** properly configured +- ✅ **Image scanning** for vulnerabilities +- ✅ **Supply chain security** with signed images + +## 🎭 Example Deployments + +### Multi-Environment Setup + +```bash +# Development +export DOCKER_IMAGE=localhost:5000/rxminder:dev +./scripts/k8s-deploy-template.sh deploy + +# Staging +export DOCKER_IMAGE=registry.company.com/rxminder:staging +./scripts/k8s-deploy-template.sh deploy + +# Production +export DOCKER_IMAGE=registry.company.com/rxminder:v1.2.0 +./scripts/k8s-deploy-template.sh deploy +``` + +This flexible Docker image configuration makes RxMinder truly **portable** and **CI/CD-ready** across any container registry and deployment environment! diff --git a/docs/deployment/GITEA_SETUP.md b/docs/deployment/GITEA_SETUP.md new file mode 100644 index 0000000..21616b1 --- /dev/null +++ b/docs/deployment/GITEA_SETUP.md @@ -0,0 +1,242 @@ +# 🦌 Gitea CI/CD Setup Complete! + +Your RxMinder app now has comprehensive Gitea Actions CI/CD support! Here's what's been created: + +## 📁 New Files Structure + +``` +.gitea/ +├── workflows/ +│ └── ci-cd.yml # Main CI/CD workflow +├── docker-compose.ci.yml # CI-specific compose override +├── gitea-bake.hcl # Gitea-optimized buildx config +└── README.md # Detailed Gitea configuration guide + +scripts/ +├── gitea-deploy.sh # Gitea-specific deployment script +└── gitea-helper.sh # Comprehensive Gitea operations helper +``` + +## 🚀 Quick Start + +### 1. **Setup Environment Configuration** + +```bash +# Copy the example environment file and customize +cp .env.example .env + +# Edit .env with your registry and configuration: +CONTAINER_REGISTRY=gitea.yourdomain.com +CONTAINER_REPOSITORY=username/rxminder +GITEA_REGISTRY=gitea.yourdomain.com +GITEA_REPOSITORY=username/rxminder +``` + +### 2. **Setup Gitea Repository** + +```bash +# Configure in Gitea Repository Settings → Actions + +# Required Secrets: +GITEA_TOKEN # Personal access token with package write permissions +VITE_COUCHDB_PASSWORD # CouchDB password +DEPLOYMENT_WEBHOOK_URL # Optional: deployment notifications + +# Repository Variables (optional - will use .env defaults): +GITEA_REGISTRY # Override registry from .env +VITE_COUCHDB_URL # http://localhost:5984 +VITE_COUCHDB_USER # admin +APP_BASE_URL # http://localhost:8080 +``` + +### 3. **Local Development with Gitea** + +```bash +# Setup Gitea buildx builder +bun run gitea:setup + +# Build for local development +bun run gitea:build-local + +# Run tests +bun run gitea:test + +# Check status +bun run gitea:status +``` + +### 4. **Production Deployment** + +```bash +# Build and push to registry +export GITEA_TOKEN=your_token +export GITEA_REGISTRY=your-gitea.com +export GITEA_REPOSITORY=username/rxminder + +bun run gitea:build-prod v1.0.0 + +# Deploy to production +bun run gitea:deploy production v1.0.0 +``` + +## 🔧 Gitea Actions Features + +### **Multi-Platform Builds** + +- ✅ AMD64 (Intel/AMD processors) +- ✅ ARM64 (Apple Silicon, AWS Graviton) +- ✅ Optimized layer caching +- ✅ Registry-based build cache + +### **Security & Quality** + +- ✅ Trivy vulnerability scanning +- ✅ Supply chain attestations (SBOM, provenance) +- ✅ Dependency auditing +- ✅ Lint and type checking + +### **Deployment Options** + +- ✅ Docker Compose deployment +- ✅ Kubernetes deployment +- ✅ Staging environment support +- ✅ Health checks and monitoring + +### **Automation** + +- ✅ Automatic builds on push/PR +- ✅ Multi-environment deployments +- ✅ Image cleanup and maintenance +- ✅ Deployment notifications + +## 📋 Available Commands + +### **Gitea Helper Script** + +```bash +./scripts/gitea-helper.sh setup # Setup buildx for Gitea +./scripts/gitea-helper.sh build-local # Local development build +./scripts/gitea-helper.sh build-multi # Multi-platform build +./scripts/gitea-helper.sh build-staging # Staging build +./scripts/gitea-helper.sh build-prod # Production build +./scripts/gitea-helper.sh test # Run all tests +./scripts/gitea-helper.sh deploy # Deploy to environment +./scripts/gitea-helper.sh status # Show CI/CD status +./scripts/gitea-helper.sh cleanup # Cleanup builders/images +``` + +### **Package.json Scripts** + +```bash +bun run gitea:setup # Setup Gitea buildx +bun run gitea:build # Multi-platform build +bun run gitea:build-local # Local development +bun run gitea:build-staging # Staging build +bun run gitea:build-prod # Production build +bun run gitea:test # Run tests +bun run gitea:deploy # Deploy application +bun run gitea:status # Check status +bun run gitea:cleanup # Cleanup +``` + +## 🎯 Workflow Triggers + +### **Automatic Triggers** + +- **Push to main/develop**: Full build, test, and deploy +- **Pull Request**: Build, test, and security scan +- **Manual dispatch**: On-demand deployment + +### **Environment-Specific** + +- **Development**: Fast single-platform builds +- **Staging**: Full testing with staging configs +- **Production**: Multi-platform with attestations + +## 🔒 Security Features + +### **Image Security** + +- Vulnerability scanning with Trivy +- Base image security updates +- Minimal attack surface +- Supply chain attestations + +### **Secrets Management** + +- Gitea-native secrets storage +- Environment-specific variables +- Token rotation support +- Secure registry authentication + +## 📊 Monitoring & Notifications + +### **Health Checks** + +- Frontend application health +- Database connectivity +- Service dependency checks +- Container resource monitoring + +### **Notifications** + +- Deployment success/failure alerts +- Security scan results +- Build status updates +- Custom webhook integration + +## 🚀 Next Steps + +1. **Configure Gitea Repository**: + - Enable Actions in repository settings + - Add required secrets and variables + - Configure container registry + +2. **Set up Gitea Runner**: + - Install and configure Gitea Actions runner + - Ensure Docker and buildx support + - Configure appropriate labels + +3. **Test the Pipeline**: + + ```bash + # Push to trigger the workflow + git add . + git commit -m "Setup Gitea CI/CD" + git push origin main + ``` + +4. **Customize for Your Environment**: + - Update registry URLs in `.gitea/gitea-bake.hcl` + - Modify deployment targets in `scripts/gitea-deploy.sh` + - Configure environment-specific variables + +## 🔄 Migration Notes + +- ✅ **Fully compatible** with existing Docker Buildx setup +- ✅ **No breaking changes** to development workflow +- ✅ **Parallel support** with GitHub Actions if needed +- ✅ **Easy rollback** - simply delete `.gitea/` directory + +Your RxMinder app is now ready for professional-grade CI/CD with Gitea! 🎉 + +## 📞 Troubleshooting + +### Common Issues: + +1. **Build failures**: Check Gitea runner has Docker buildx +2. **Registry push errors**: Verify GITEA_TOKEN permissions +3. **Deployment issues**: Check environment variables and secrets + +### Debug Commands: + +```bash +# Check Gitea environment +./scripts/gitea-helper.sh status + +# Test local build +./scripts/gitea-helper.sh build-local + +# Verify registry login +docker login your-gitea.com +``` diff --git a/docs/deployment/STORAGE_CONFIGURATION.md b/docs/deployment/STORAGE_CONFIGURATION.md new file mode 100644 index 0000000..1c326bc --- /dev/null +++ b/docs/deployment/STORAGE_CONFIGURATION.md @@ -0,0 +1,226 @@ +# 📦 Storage Configuration Examples + +## Overview + +RxMinder now supports configurable storage through environment variables, making it easy to adapt to different Kubernetes environments and storage requirements. + +## 🗂️ Storage Configuration Variables + +### **STORAGE_CLASS** + +The Kubernetes StorageClass to use for persistent volumes. + +**Common Options:** + +- `longhorn` - Longhorn distributed storage (Raspberry Pi clusters) +- `local-path` - Local path provisioner (k3s default) +- `standard` - Cloud provider standard storage +- `fast-ssd` - High-performance SSD storage +- `gp2` - AWS General Purpose SSD +- `pd-standard` - Google Cloud Standard Persistent Disk +- `azure-disk` - Azure Standard Disk + +### **STORAGE_SIZE** + +The amount of storage to allocate for the CouchDB database. + +**Sizing Guidelines:** + +- `1Gi` - Minimal testing (not recommended for production) +- `5Gi` - Small deployment (default, good for development) +- `10Gi` - Medium deployment (suitable for small teams) +- `20Gi` - Large deployment (production use) +- `50Gi+` - Enterprise deployment (high-volume usage) + +## 🎯 Environment-Specific Examples + +### Development (.env) + +```bash +# Development environment +APP_NAME=rxminder-dev +STORAGE_CLASS=local-path +STORAGE_SIZE=5Gi +INGRESS_HOST=rxminder-dev.local +``` + +### Staging (.env.staging) + +```bash +# Staging environment +APP_NAME=rxminder-staging +STORAGE_CLASS=longhorn +STORAGE_SIZE=10Gi +INGRESS_HOST=staging.rxminder.company.com +``` + +### Production (.env.production) + +```bash +# Production environment +APP_NAME=rxminder +STORAGE_CLASS=fast-ssd +STORAGE_SIZE=50Gi +INGRESS_HOST=rxminder.company.com +``` + +### Cloud Providers + +#### AWS EKS + +```bash +APP_NAME=rxminder +STORAGE_CLASS=gp2 # General Purpose SSD +STORAGE_SIZE=20Gi +INGRESS_HOST=rxminder.aws.company.com +``` + +#### Google GKE + +```bash +APP_NAME=rxminder +STORAGE_CLASS=pd-standard # Standard Persistent Disk +STORAGE_SIZE=20Gi +INGRESS_HOST=rxminder.gcp.company.com +``` + +#### Azure AKS + +```bash +APP_NAME=rxminder +STORAGE_CLASS=managed-premium # Premium SSD +STORAGE_SIZE=20Gi +INGRESS_HOST=rxminder.azure.company.com +``` + +## 🏗️ Generated Kubernetes Resources + +### Before (Hardcoded) + +```yaml +# Old approach - hardcoded values +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: couchdb-pvc +spec: + storageClassName: longhorn + resources: + requests: + storage: 1Gi +``` + +### After (Template-Based) + +```yaml +# Template: k8s/couchdb-pvc.yaml.template +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ${APP_NAME}-couchdb-pvc + labels: + app: ${APP_NAME} +spec: + storageClassName: ${STORAGE_CLASS} + resources: + requests: + storage: ${STORAGE_SIZE} +``` + +### Deployed Result + +```yaml +# After envsubst processing +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rxminder-couchdb-pvc + labels: + app: rxminder +spec: + storageClassName: fast-ssd + resources: + requests: + storage: 20Gi +``` + +## 🚀 Deployment Examples + +### Quick Development Setup + +```bash +# Development with local storage +export APP_NAME=rxminder-dev +export STORAGE_CLASS=local-path +export STORAGE_SIZE=5Gi +./scripts/k8s-deploy-template.sh deploy +``` + +### Production Deployment + +```bash +# Copy production environment +cp .env.production .env +# Edit with your specific values +nano .env + +# Deploy to production +./scripts/k8s-deploy-template.sh deploy +``` + +### Custom Configuration + +```bash +# Override specific values +export STORAGE_CLASS=custom-storage +export STORAGE_SIZE=100Gi +./scripts/k8s-deploy-template.sh deploy +``` + +## 🔍 Storage Class Discovery + +### Find Available Storage Classes + +```bash +# List available storage classes in your cluster +kubectl get storageclass + +# Get details about a specific storage class +kubectl describe storageclass longhorn +``` + +### Common Storage Class Names by Platform + +| Platform | Common Storage Classes | +| -------------- | ------------------------------------------ | +| **k3s** | `local-path` (default) | +| **Longhorn** | `longhorn` | +| **AWS EKS** | `gp2`, `gp3`, `io1`, `io2` | +| **Google GKE** | `standard`, `ssd`, `pd-standard`, `pd-ssd` | +| **Azure AKS** | `default`, `managed-premium` | +| **Rancher** | `longhorn`, `local-path` | + +## 💡 Benefits + +### Flexibility + +- ✅ **Environment-specific** storage configuration +- ✅ **Cloud-agnostic** deployment +- ✅ **Performance tuning** via storage class selection +- ✅ **Cost optimization** through appropriate sizing + +### Maintainability + +- ✅ **Single source of truth** via `.env` files +- ✅ **Easy scaling** by changing STORAGE_SIZE +- ✅ **Environment promotion** using different .env files +- ✅ **Disaster recovery** with consistent configurations + +### Developer Experience + +- ✅ **No hardcoded values** in manifests +- ✅ **Clear documentation** of requirements +- ✅ **Validation** of required variables +- ✅ **Automated deployment** with proper storage setup + +This approach makes RxMinder truly **portable** across different Kubernetes environments while maintaining **production-grade** storage management! diff --git a/docs/development/API.md b/docs/development/API.md new file mode 100644 index 0000000..332d889 --- /dev/null +++ b/docs/development/API.md @@ -0,0 +1,839 @@ +# API Documentation + +## 📚 API Reference for Medication Reminder App + +### **Base URL** + +- Development: `http://localhost:5173` +- Production: `http://localhost:8080` + +### **Authentication** + +All authenticated endpoints require a valid session token. + +#### **Headers** + +```http +Authorization: Bearer +Content-Type: application/json +``` + +--- + +## 🔐 Authentication Endpoints + +### **Register User** + +Create a new user account with email verification. + +**Endpoint:** `POST /auth/register` + +**Request Body:** + +```json +{ + "email": "user@example.com", + "password": "SecurePassword123!", + "username": "JohnDoe" +} +``` + +**Response:** + +```json +{ + "user": { + "_id": "user-uuid", + "email": "user@example.com", + "username": "JohnDoe", + "status": "PENDING", + "emailVerified": false, + "role": "USER", + "createdAt": "2025-09-05T12:00:00Z" + }, + "verificationToken": { + "token": "verification-token-uuid", + "expiresAt": "2025-09-05T13:00:00Z" + } +} +``` + +**Status Codes:** + +- `201` - User created successfully +- `400` - Invalid input data +- `409` - Email already exists + +--- + +### **Login User** + +Authenticate user with email and password. + +**Endpoint:** `POST /auth/login` + +**Request Body:** + +```json +{ + "email": "user@example.com", + "password": "SecurePassword123!" +} +``` + +**Response:** + +```json +{ + "user": { + "_id": "user-uuid", + "email": "user@example.com", + "username": "JohnDoe", + "status": "ACTIVE", + "emailVerified": true, + "role": "USER" + }, + "accessToken": "jwt-access-token", + "refreshToken": "jwt-refresh-token" +} +``` + +**Status Codes:** + +- `200` - Login successful +- `401` - Invalid credentials +- `403` - Account not verified or suspended + +--- + +### **OAuth Login** + +Authenticate using OAuth providers (Google, GitHub). + +**Endpoint:** `POST /auth/oauth` + +**Request Body:** + +```json +{ + "provider": "google", + "userData": { + "email": "user@example.com", + "username": "John Doe", + "avatar": "https://example.com/avatar.jpg" + } +} +``` + +**Response:** + +```json +{ + "user": { + "_id": "user-uuid", + "email": "user@example.com", + "username": "John Doe", + "status": "ACTIVE", + "emailVerified": true, + "role": "USER", + "avatar": "https://example.com/avatar.jpg" + }, + "accessToken": "jwt-access-token", + "refreshToken": "jwt-refresh-token" +} +``` + +--- + +### **Verify Email** + +Activate user account using verification token. + +**Endpoint:** `POST /auth/verify-email` + +**Request Body:** + +```json +{ + "token": "verification-token-uuid" +} +``` + +**Response:** + +```json +{ + "user": { + "_id": "user-uuid", + "email": "user@example.com", + "username": "JohnDoe", + "status": "ACTIVE", + "emailVerified": true, + "role": "USER" + } +} +``` + +--- + +### **Change Password** + +Change user password (requires current password). + +**Endpoint:** `POST /auth/change-password` + +**Request Body:** + +```json +{ + "userId": "user-uuid", + "currentPassword": "OldPassword123!", + "newPassword": "NewPassword456!" +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Password changed successfully" +} +``` + +--- + +### **Request Password Reset** + +Request password reset email. + +**Endpoint:** `POST /auth/request-password-reset` + +**Request Body:** + +```json +{ + "email": "user@example.com" +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Password reset email sent" +} +``` + +--- + +### **Reset Password** + +Reset password using reset token. + +**Endpoint:** `POST /auth/reset-password` + +**Request Body:** + +```json +{ + "token": "reset-token-uuid", + "newPassword": "NewPassword123!" +} +``` + +**Response:** + +```json +{ + "success": true, + "message": "Password reset successful" +} +``` + +--- + +## 💊 Medication Management + +### **Add Medication** + +Add a new medication to user's list. + +**Endpoint:** `POST /medications` + +**Request Body:** + +```json +{ + "name": "Aspirin", + "dosage": "100mg", + "frequency": "Daily", + "startTime": "08:00", + "notes": "Take with food", + "icon": "💊" +} +``` + +**Response:** + +```json +{ + "_id": "medication-uuid", + "name": "Aspirin", + "dosage": "100mg", + "frequency": "Daily", + "startTime": "08:00", + "notes": "Take with food", + "icon": "💊", + "userId": "user-uuid", + "createdAt": "2025-09-05T12:00:00Z" +} +``` + +--- + +### **Get Medications** + +Retrieve user's medications. + +**Endpoint:** `GET /medications` + +**Query Parameters:** + +- `active` (boolean) - Filter active medications only + +**Response:** + +```json +[ + { + "_id": "medication-uuid", + "name": "Aspirin", + "dosage": "100mg", + "frequency": "Daily", + "startTime": "08:00", + "notes": "Take with food", + "icon": "💊" + } +] +``` + +--- + +### **Update Medication** + +Update existing medication. + +**Endpoint:** `PUT /medications/:id` + +**Request Body:** + +```json +{ + "dosage": "200mg", + "notes": "Take with plenty of water" +} +``` + +**Response:** + +```json +{ + "_id": "medication-uuid", + "name": "Aspirin", + "dosage": "200mg", + "frequency": "Daily", + "startTime": "08:00", + "notes": "Take with plenty of water", + "icon": "💊" +} +``` + +--- + +### **Delete Medication** + +Remove medication from user's list. + +**Endpoint:** `DELETE /medications/:id` + +**Response:** + +```json +{ + "success": true, + "message": "Medication deleted successfully" +} +``` + +--- + +## ⏰ Reminder Management + +### **Add Custom Reminder** + +Create a custom reminder. + +**Endpoint:** `POST /reminders` + +**Request Body:** + +```json +{ + "title": "Doctor Appointment", + "message": "Annual checkup with Dr. Smith", + "scheduledFor": "2025-09-15T14:00:00Z", + "recurrence": "yearly" +} +``` + +**Response:** + +```json +{ + "_id": "reminder-uuid", + "title": "Doctor Appointment", + "message": "Annual checkup with Dr. Smith", + "scheduledFor": "2025-09-15T14:00:00Z", + "recurrence": "yearly", + "userId": "user-uuid", + "isActive": true +} +``` + +--- + +### **Get Reminders** + +Retrieve user's reminders. + +**Endpoint:** `GET /reminders` + +**Query Parameters:** + +- `date` (string) - Filter by specific date (YYYY-MM-DD) +- `active` (boolean) - Filter active reminders only + +**Response:** + +```json +[ + { + "_id": "reminder-uuid", + "title": "Doctor Appointment", + "message": "Annual checkup with Dr. Smith", + "scheduledFor": "2025-09-15T14:00:00Z", + "recurrence": "yearly", + "isActive": true + } +] +``` + +--- + +## 📊 Dose Tracking + +### **Record Taken Dose** + +Mark a dose as taken. + +**Endpoint:** `POST /doses/taken` + +**Request Body:** + +```json +{ + "medicationId": "medication-uuid", + "scheduledTime": "2025-09-05T08:00:00Z", + "takenAt": "2025-09-05T08:15:00Z", + "notes": "Took with breakfast" +} +``` + +**Response:** + +```json +{ + "success": true, + "dose": { + "id": "medication-uuid-2025-09-05", + "medicationId": "medication-uuid", + "scheduledTime": "2025-09-05T08:00:00Z", + "takenAt": "2025-09-05T08:15:00Z", + "status": "TAKEN", + "notes": "Took with breakfast" + } +} +``` + +--- + +### **Get Dose History** + +Retrieve dose history for analytics. + +**Endpoint:** `GET /doses` + +**Query Parameters:** + +- `medicationId` (string) - Filter by medication +- `startDate` (string) - Start date (YYYY-MM-DD) +- `endDate` (string) - End date (YYYY-MM-DD) +- `status` (string) - Filter by status (TAKEN, MISSED, UPCOMING) + +**Response:** + +```json +{ + "doses": [ + { + "id": "medication-uuid-2025-09-05", + "medicationId": "medication-uuid", + "scheduledTime": "2025-09-05T08:00:00Z", + "takenAt": "2025-09-05T08:15:00Z", + "status": "TAKEN" + } + ], + "stats": { + "totalDoses": 30, + "takenDoses": 28, + "missedDoses": 2, + "adherenceRate": 93.3 + } +} +``` + +--- + +## 👑 Admin Endpoints + +### **Get All Users** + +Retrieve all users (admin only). + +**Endpoint:** `GET /admin/users` + +**Query Parameters:** + +- `status` (string) - Filter by status +- `role` (string) - Filter by role +- `page` (number) - Pagination page +- `limit` (number) - Items per page + +**Response:** + +```json +{ + "users": [ + { + "_id": "user-uuid", + "email": "user@example.com", + "username": "JohnDoe", + "status": "ACTIVE", + "role": "USER", + "emailVerified": true, + "createdAt": "2025-09-05T12:00:00Z", + "lastLoginAt": "2025-09-05T15:30:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 150, + "pages": 8 + } +} +``` + +--- + +### **Update User Status** + +Change user account status (admin only). + +**Endpoint:** `PUT /admin/users/:id/status` + +**Request Body:** + +```json +{ + "status": "SUSPENDED" +} +``` + +**Response:** + +```json +{ + "success": true, + "user": { + "_id": "user-uuid", + "status": "SUSPENDED" + } +} +``` + +--- + +### **Delete User** + +Delete user account (admin only). + +**Endpoint:** `DELETE /admin/users/:id` + +**Response:** + +```json +{ + "success": true, + "message": "User deleted successfully" +} +``` + +--- + +## 📈 Analytics + +### **User Statistics** + +Get user's medication adherence statistics. + +**Endpoint:** `GET /analytics/stats` + +**Query Parameters:** + +- `period` (string) - Time period (7d, 30d, 90d, 1y) + +**Response:** + +```json +{ + "adherence": { + "overall": 92.5, + "trend": "improving", + "streak": 7 + }, + "medications": [ + { + "medicationId": "medication-uuid", + "name": "Aspirin", + "taken": 28, + "missed": 2, + "adherence": 93.3 + } + ], + "dailyStats": [ + { + "date": "2025-09-05", + "adherence": 100, + "totalDoses": 3, + "takenDoses": 3 + } + ] +} +``` + +--- + +## 🔧 User Settings + +### **Get User Settings** + +Retrieve user preferences. + +**Endpoint:** `GET /settings` + +**Response:** + +```json +{ + "notifications": { + "email": true, + "push": false, + "reminderSound": true + }, + "preferences": { + "theme": "dark", + "timezone": "UTC-5", + "dateFormat": "MM/DD/YYYY" + }, + "privacy": { + "shareStats": false, + "anonymousUsage": true + } +} +``` + +--- + +### **Update User Settings** + +Update user preferences. + +**Endpoint:** `PUT /settings` + +**Request Body:** + +```json +{ + "notifications": { + "email": false, + "push": true + }, + "preferences": { + "theme": "light" + } +} +``` + +**Response:** + +```json +{ + "success": true, + "settings": { + "notifications": { + "email": false, + "push": true, + "reminderSound": true + }, + "preferences": { + "theme": "light", + "timezone": "UTC-5", + "dateFormat": "MM/DD/YYYY" + } + } +} +``` + +--- + +## 📁 File Upload + +### **Upload Avatar** + +Upload user avatar image. + +**Endpoint:** `POST /upload/avatar` + +**Request:** Multipart form data + +- `avatar` (file) - Image file (JPEG, PNG, max 2MB) + +**Response:** + +```json +{ + "success": true, + "avatarUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." +} +``` + +--- + +## 🔍 Search + +### **Search Medications** + +Search for medications by name. + +**Endpoint:** `GET /search/medications` + +**Query Parameters:** + +- `q` (string) - Search query +- `limit` (number) - Max results + +**Response:** + +```json +{ + "results": [ + { + "name": "Aspirin", + "commonDosages": ["100mg", "325mg", "500mg"], + "category": "Pain Reliever" + } + ] +} +``` + +--- + +## ❌ Error Responses + +### **Error Format** + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid input data", + "details": { + "email": "Invalid email format", + "password": "Password too weak" + } + } +} +``` + +### **Common Error Codes** + +- `VALIDATION_ERROR` (400) - Invalid input data +- `UNAUTHORIZED` (401) - Authentication required +- `FORBIDDEN` (403) - Insufficient permissions +- `NOT_FOUND` (404) - Resource not found +- `CONFLICT` (409) - Resource already exists +- `RATE_LIMITED` (429) - Too many requests +- `INTERNAL_ERROR` (500) - Server error + +--- + +## 📊 Rate Limiting + +### **Limits** + +- Authentication endpoints: 5 requests/minute +- General API: 100 requests/minute +- Upload endpoints: 10 requests/minute + +### **Headers** + +```http +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1693929600 +``` + +--- + +## 🔒 Security + +### **Authentication** + +- JWT tokens with 24-hour expiration +- Refresh tokens for automatic renewal +- Secure password hashing with bcrypt + +### **Authorization** + +- Role-based access control (USER, ADMIN) +- Resource-level permissions +- Account status validation + +### **Data Protection** + +- Input validation and sanitization +- SQL injection prevention +- XSS protection +- CORS configuration + +--- + +## 📚 Additional Resources + +- [Postman Collection](./postman-collection.json) +- [OpenAPI Specification](./openapi.yaml) +- [SDK Documentation](./sdk-docs.md) +- [Integration Examples](./examples/) diff --git a/docs/development/APPLICATION_SECURITY.md b/docs/development/APPLICATION_SECURITY.md new file mode 100644 index 0000000..2bd41bb --- /dev/null +++ b/docs/development/APPLICATION_SECURITY.md @@ -0,0 +1,162 @@ +# Security Guide + +## 🔐 Security Best Practices for Medication Reminder App + +### **Password Security** + +#### **Password Requirements** + +- Minimum 8 characters +- Must contain uppercase and lowercase letters +- Must contain at least one number +- Must contain at least one special character +- Cannot be common passwords (password123, admin, etc.) + +#### **Password Hashing** + +- Uses bcrypt with salt rounds for secure password storage +- Passwords are never stored in plain text +- Password verification happens through secure hash comparison + +### **Authentication Security** + +#### **Session Management** + +- JWT-like token system for user sessions +- Tokens have expiration times +- Secure token storage and transmission +- Automatic session cleanup on logout + +#### **Email Verification** + +- All new accounts require email verification +- Verification tokens are time-limited +- Prevents unauthorized account creation +- Uses cryptographically secure random tokens + +#### **OAuth Security** + +- Supports Google and GitHub OAuth +- Secure OAuth flow implementation +- No password storage for OAuth users +- Account linking prevention for security + +### **Environment Security** + +#### **Environment Variables** + +- Never commit `.env` files to version control +- Use separate environment files for different deployments +- Rotate credentials regularly +- Use strong, unique passwords for each environment + +#### **Docker Security** + +- Non-root user for application execution +- Multi-stage builds to minimize attack surface +- Health checks for service monitoring +- Isolated network for services + +### **Database Security** + +#### **CouchDB Security** + +- Admin authentication required +- Database-level access control +- SSL/TLS encryption for production +- Regular backup and security updates + +#### **Data Protection** + +- User data isolation by user ID +- Input validation and sanitization +- Protection against injection attacks +- Secure data deletion capabilities + +### **Production Security Checklist** + +#### **Before Deployment** + +- [ ] Change default admin password +- [ ] Configure strong CouchDB credentials +- [ ] Set up Mailgun with proper API keys +- [ ] Enable SSL/TLS certificates +- [ ] Configure firewall rules +- [ ] Set up monitoring and logging + +#### **Regular Security Tasks** + +- [ ] Rotate credentials monthly +- [ ] Update dependencies regularly +- [ ] Monitor logs for suspicious activity +- [ ] Backup databases securely +- [ ] Review user access permissions +- [ ] Test disaster recovery procedures + +### **Incident Response** + +#### **Security Breach Protocol** + +1. **Immediate Response** + - Disable affected accounts + - Change all credentials + - Review access logs + - Document the incident + +2. **Investigation** + - Identify breach source + - Assess data exposure + - Notify affected users + - Implement fixes + +3. **Recovery** + - Restore from secure backups + - Update security measures + - Monitor for further issues + - Conduct post-incident review + +### **Compliance Considerations** + +#### **Data Privacy** + +- User data minimization +- Right to data deletion +- Transparent privacy policy +- Secure data export capabilities + +#### **Healthcare Compliance** + +- HIPAA considerations for health data +- Secure medication information handling +- Audit trail capabilities +- Data retention policies + +### **Security Monitoring** + +#### **Logging** + +- Authentication attempts +- Failed login monitoring +- Admin actions tracking +- Database access logging + +#### **Alerting** + +- Multiple failed login attempts +- Admin privilege escalation +- Unusual data access patterns +- System health issues + +### **Emergency Contacts** + +#### **Security Issues** + +- Development Team: security@your-domain.com +- System Administrator: admin@your-domain.com +- Emergency Response: +1-XXX-XXX-XXXX + +#### **Third-party Services** + +- Mailgun Support: support@mailgun.com +- CouchDB Security: security@apache.org +- Docker Security: security@docker.com diff --git a/docs/development/CODE_QUALITY.md b/docs/development/CODE_QUALITY.md new file mode 100644 index 0000000..a063b1b --- /dev/null +++ b/docs/development/CODE_QUALITY.md @@ -0,0 +1,246 @@ +# Code Quality and Formatting Setup + +This project includes comprehensive code quality tools and pre-commit hooks to maintain consistent code standards. + +## Tools Configured + +### Pre-commit Hooks + +- **File formatting**: Trailing whitespace, end-of-file fixes, line ending normalization +- **Security**: Private key detection, secrets scanning with detect-secrets +- **Linting**: ESLint for TypeScript/JavaScript, Hadolint for Docker, ShellCheck for scripts +- **Type checking**: TypeScript compilation checks +- **Formatting**: Prettier for code formatting, Markdownlint for documentation + +### Code Formatters + +- **Prettier**: Handles JavaScript, TypeScript, JSON, YAML, Markdown, CSS, SCSS, HTML formatting +- **ESLint**: TypeScript/JavaScript linting with comprehensive rules and React hooks support +- **EditorConfig**: Consistent coding styles across editors + +### Security Tools + +- **detect-secrets**: Prevents secrets from being committed to the repository +- **Private key detection**: Automatically detects and blocks private keys + +## Setup + +Run the setup script to install all tools and configure pre-commit hooks: + +```bash +./scripts/setup-pre-commit.sh +``` + +Alternatively, install manually: + +```bash +# Install dependencies +bun install + +# Install pre-commit in Python virtual environment +python -m venv .venv +source .venv/bin/activate # or .venv/Scripts/activate on Windows +pip install pre-commit detect-secrets + +# Install pre-commit hooks +pre-commit install + +# Create secrets baseline (if it doesn't exist) +detect-secrets scan --baseline .secrets.baseline +``` + +## Usage + +### Automatic (Recommended) + +Pre-commit hooks will automatically run on every commit, ensuring: + +- Code is properly formatted +- Linting rules are followed +- Type checking passes +- No secrets are committed + +### Manual Commands + +```bash +# Format all files +bun run format + +# Check formatting without fixing +bun run format:check + +# Lint TypeScript/JavaScript files +bun run lint + +# Lint with auto-fix +bun run lint:fix + +# Type checking +bun run type-check + +# Run pre-commit hook +bun run pre-commit + +# Run all pre-commit hooks manually (using virtual environment) +/home/will/Code/meds/.venv/bin/pre-commit run --all-files + +# Run specific hook +/home/will/Code/meds/.venv/bin/pre-commit run prettier --all-files + +# Update pre-commit hook versions +/home/will/Code/meds/.venv/bin/pre-commit autoupdate +``` + +## Configuration Files + +- `.pre-commit-config.yaml` - Pre-commit hooks configuration with comprehensive security and quality checks +- `.prettierrc` - Prettier formatting rules with TypeScript/React optimizations +- `.prettierignore` - Files to ignore for Prettier formatting +- `.editorconfig` - Editor configuration for consistent coding styles across IDEs +- `eslint.config.cjs` - ESLint linting rules with TypeScript and React hooks support +- `.markdownlint.json` - Markdown linting configuration for documentation quality +- `.secrets.baseline` - Baseline for detect-secrets security scanning +- `scripts/setup-pre-commit.sh` - Automated setup script for all tools +- `docs/CODE_QUALITY.md` - This documentation file + +## Pre-commit Hook Details + +The following hooks run automatically on every commit: + +### File Quality Hooks + +- `trailing-whitespace` - Removes trailing whitespace +- `end-of-file-fixer` - Ensures files end with newline +- `check-yaml` - Validates YAML syntax +- `check-json` - Validates JSON syntax +- `check-toml` - Validates TOML syntax +- `check-xml` - Validates XML syntax +- `check-merge-conflict` - Prevents merge conflict markers +- `check-added-large-files` - Prevents large files from being committed +- `check-case-conflict` - Prevents case conflicts on case-insensitive filesystems +- `check-symlinks` - Validates symlinks +- `mixed-line-ending` - Ensures consistent line endings (LF) + +### Security Hooks + +- `detect-private-key` - Prevents private keys from being committed +- `detect-secrets` - Scans for secrets using baseline comparison + +### Code Quality Hooks + +- `prettier` - Formats JavaScript, TypeScript, JSON, YAML, Markdown, CSS, SCSS, HTML +- `eslint` - Lints TypeScript/JavaScript with auto-fix +- `tsc` - TypeScript type checking + +### Infrastructure Hooks + +- `hadolint-docker` - Lints Dockerfile files +- `shellcheck` - Lints shell scripts +- `markdownlint` - Lints and formats Markdown files + +## IDE Integration + +### VS Code (Recommended) + +Install these extensions for optimal integration: + +- **Prettier - Code formatter** (`esbenp.prettier-vscode`) +- **ESLint** (`dbaeumer.vscode-eslint`) +- **EditorConfig for VS Code** (`editorconfig.editorconfig`) + +### Settings for VS Code + +Add these settings to your VS Code `settings.json`: + +```json +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"] +} +``` + +### Other IDEs + +Most modern IDEs support EditorConfig, Prettier, and ESLint through plugins: + +- **WebStorm/IntelliJ**: Built-in support for most tools +- **Vim/Neovim**: Use coc.nvim or native LSP with appropriate plugins +- **Sublime Text**: Install Package Control packages for each tool + +## Customization + +### Prettier + +Edit `.prettierrc` to modify formatting rules. + +### ESLint + +Edit `eslint.config.cjs` to add or modify linting rules. + +### Pre-commit + +Edit `.pre-commit-config.yaml` to add, remove, or modify hooks. + +## Troubleshooting + +### Pre-commit hooks failing + +```bash +# Skip hooks temporarily (not recommended) +git commit --no-verify -m "commit message" + +# Fix issues and try again +bun run lint:fix +bun run format +git add . +git commit -m "commit message" +``` + +### Update hook versions + +```bash +/home/will/Code/meds/.venv/bin/pre-commit autoupdate +``` + +### Clear pre-commit cache + +```bash +/home/will/Code/meds/.venv/bin/pre-commit clean +``` + +### Secrets detection issues + +```bash +# Update secrets baseline +/home/will/Code/meds/.venv/bin/detect-secrets scan --update .secrets.baseline + +# Audit detected secrets +/home/will/Code/meds/.venv/bin/detect-secrets audit .secrets.baseline +``` + +### Python virtual environment issues + +```bash +# Recreate virtual environment +rm -rf .venv +python -m venv .venv +source .venv/bin/activate +pip install pre-commit detect-secrets +/home/will/Code/meds/.venv/bin/pre-commit install +``` + +### ESLint configuration issues + +If you encounter TypeScript project configuration errors: + +```bash +# Ensure tsconfig.json is properly configured +bun run type-check + +# Check ESLint configuration +npx eslint --print-config index.tsx +``` diff --git a/docs/development/SECURITY_CHANGES.md b/docs/development/SECURITY_CHANGES.md new file mode 100644 index 0000000..a7e123b --- /dev/null +++ b/docs/development/SECURITY_CHANGES.md @@ -0,0 +1,148 @@ +# 🔐 Security Changes Summary + +## Overview + +We have systematically removed all hardcoded credentials from the RxMinder application and replaced them with secure defaults and environment variables. + +## ✅ Changes Made + +### 1. Kubernetes Configuration + +- **`k8s/couchdb-secret.yaml`**: Converted to template with secure base64-encoded defaults +- **`k8s/db-seed-job.yaml`**: Now uses environment variables from secrets instead of hardcoded credentials + +### 2. Docker Configuration + +- **`docker/Dockerfile`**: Updated default password arguments to secure values +- **`docker/docker-compose.yaml`**: All password environment variables use secure fallbacks +- **`docker/docker-bake.hcl`**: Updated variable defaults to secure passwords + +### 3. Shell Scripts + +Updated all deployment and build scripts with secure password fallbacks: + +- `scripts/setup.sh` +- `scripts/deploy.sh` +- `scripts/validate-deployment.sh` +- `scripts/buildx-helper.sh` +- `scripts/gitea-deploy.sh` +- `scripts/gitea-helper.sh` +- `scripts/seed-production.js` +- `rename-app.sh` + +### 4. CI/CD Workflows + +- **`.github/workflows/build-deploy.yml`**: Updated fallback passwords to secure values +- **`.gitea/workflows/ci-cd.yml`**: Updated fallback passwords to secure values +- **`.gitea/docker-compose.ci.yml`**: Updated test database passwords +- **`.gitea/gitea-bake.hcl`**: Updated default password variables + +### 5. Environment Files + +- **`.env.example`**: Updated with secure default passwords and documentation +- **`.env.production`**: Updated with secure default passwords +- **`test.env`**: Updated test credentials to secure values + +### 6. Documentation + +- **`README.md`**: Updated default admin credentials documentation +- **`SECURITY.md`**: Created comprehensive security guide with checklists +- **`.gitea/README.md`**: Updated documentation +- **`GITEA_SETUP.md`**: Updated setup instructions + +## 🛡️ Security Improvements + +### Before + +- Hardcoded `admin123!` and `password` throughout configuration files +- Weak default passwords in CI/CD systems +- No security documentation or guidelines + +### After + +- All passwords use environment variables or Kubernetes secrets +- Secure fallback passwords (`change-this-secure-password`) +- Comprehensive security documentation and checklists +- CI/CD systems use repository secrets with secure fallbacks + +## 🔄 Required Actions + +**CRITICAL**: Before production deployment, you must: + +1. **Update Kubernetes Secrets**: + + ```bash + # Update k8s/couchdb-secret.yaml with your own secure base64-encoded credentials + echo -n "your-secure-password" | base64 + ``` + +2. **Update Environment Variables**: + + ```bash + # Update .env and .env.production with your secure passwords + COUCHDB_PASSWORD=your-very-secure-password + VITE_COUCHDB_PASSWORD=your-very-secure-password + ``` + +3. **Configure CI/CD Secrets**: + - Set `VITE_COUCHDB_PASSWORD` in repository secrets + - Set `GITEA_TOKEN` / `GITHUB_TOKEN` for registry authentication + +4. **Review Security Checklist**: + - Follow the checklist in `SECURITY.md` + - Use strong passwords (16+ characters, mixed case, numbers, symbols) + - Enable TLS/SSL for all external communications + +## 📝 Files Modified + +### Configuration Files (11) + +- `k8s/couchdb-secret.yaml` +- `k8s/db-seed-job.yaml` +- `docker/Dockerfile` +- `docker/docker-compose.yaml` +- `docker/docker-bake.hcl` +- `.env.example` +- `.env.production` +- `test.env` +- `.github/workflows/build-deploy.yml` +- `.gitea/workflows/ci-cd.yml` +- `.gitea/docker-compose.ci.yml` +- `.gitea/gitea-bake.hcl` + +### Scripts (8) + +- `scripts/setup.sh` +- `scripts/deploy.sh` +- `scripts/validate-deployment.sh` +- `scripts/buildx-helper.sh` +- `scripts/gitea-deploy.sh` +- `scripts/gitea-helper.sh` +- `scripts/seed-production.js` +- `rename-app.sh` + +### Documentation (5) + +- `README.md` +- `SECURITY.md` (created) +- `SECURITY_CHANGES.md` (this file) +- `.gitea/README.md` +- `GITEA_SETUP.md` + +## ✅ Verification + +To verify no hardcoded credentials remain: + +```bash +# Check for insecure passwords (should return only secure defaults) +grep -r "admin123\|password[^-]\|testpassword" --include="*.yaml" --include="*.yml" --include="*.sh" --include="*.env" --include="*.js" --include="*.hcl" . + +# The only matches should be: +# - "change-this-secure-password" (secure fallback) +# - "test-secure-password" (secure test credentials) +# - Test files (acceptable for testing) +``` + +## 🎯 Result + +RxMinder is now production-ready with secure credential management. All sensitive data is properly externalized to environment variables and Kubernetes secrets, with comprehensive documentation to guide secure deployment. diff --git a/docs/migration/BUILDX_MIGRATION.md b/docs/migration/BUILDX_MIGRATION.md new file mode 100644 index 0000000..3b82a81 --- /dev/null +++ b/docs/migration/BUILDX_MIGRATION.md @@ -0,0 +1,119 @@ +# Docker Buildx Migration Complete ✅ + +Your project has been successfully migrated to use Docker Buildx for multi-platform container builds! + +## What's New + +### 🚀 Multi-Platform Support + +- **AMD64 (x86_64)**: Traditional Intel/AMD processors +- **ARM64 (aarch64)**: Apple Silicon, AWS Graviton, Raspberry Pi 4+ + +### 🛠️ New Tools & Scripts + +#### **buildx-helper.sh** - Comprehensive buildx management + +```bash +# Setup buildx builder (one-time setup) +./scripts/buildx-helper.sh setup + +# Build for local platform only (faster development) +./scripts/buildx-helper.sh build-local + +# Build for multiple platforms +./scripts/buildx-helper.sh build-multi + +# Build and push to registry +./scripts/buildx-helper.sh push docker.io/username latest + +# Build using Docker Bake (advanced) +./scripts/buildx-helper.sh bake + +# Inspect builder capabilities +./scripts/buildx-helper.sh inspect + +# Cleanup builder +./scripts/buildx-helper.sh cleanup +``` + +#### **Package.json Scripts** + +```bash +# Quick access via npm/bun scripts +bun run docker:setup # Setup buildx +bun run docker:build # Multi-platform build +bun run docker:build-local # Local platform only +bun run docker:bake # Advanced bake build +bun run docker:inspect # Inspect builder +bun run docker:cleanup # Cleanup +``` + +### 📁 New Files Added + +1. **`docker/docker-bake.hcl`** - Advanced buildx configuration +2. **`scripts/buildx-helper.sh`** - Buildx management script +3. **`.github/workflows/build-deploy.yml`** - CI/CD with buildx + +### 🔧 Updated Files + +1. **`docker/Dockerfile`** - Added NODE_ENV build arg +2. **`docker/docker-compose.yaml`** - Added multi-platform support +3. **`scripts/setup.sh`** - Updated to use buildx +4. **`scripts/validate-deployment.sh`** - Updated to use buildx +5. **`scripts/deploy.sh`** - Updated to use buildx +6. **`docker/README.md`** - Added buildx documentation +7. **`package.json`** - Added docker scripts + +## Benefits + +### 🎯 **Better Performance** + +- Enhanced caching with BuildKit +- Parallel multi-platform builds +- Faster incremental builds + +### 🌍 **Cross-Platform Compatibility** + +- Deploy on ARM-based servers (AWS Graviton, Apple Silicon) +- Support for various architectures out of the box +- Future-proof for emerging platforms + +### 🔒 **Enhanced Security** + +- Supply chain attestations (SBOM, provenance) +- Secure multi-stage builds +- Container image signing support + +### 🔄 **CI/CD Ready** + +- GitHub Actions workflow included +- Registry caching optimized +- Automated multi-platform pushes + +## Next Steps + +1. **Test the setup**: + + ```bash + bun run docker:setup + bun run docker:build-local + ``` + +2. **Configure registry** (optional): + + ```bash + ./scripts/buildx-helper.sh push ghcr.io/yourusername latest + ``` + +3. **Enable GitHub Actions** (optional): + - Push to GitHub to trigger the workflow + - Configure registry secrets if needed + +## Migration Notes + +- ✅ Backwards compatible with existing Docker commands +- ✅ Docker Compose still works as before +- ✅ All existing scripts updated to use buildx +- ✅ No breaking changes to development workflow + +Your project now supports cutting-edge multi-platform container builds! 🎉 diff --git a/docs/migration/NODEJS_PRECOMMIT_MIGRATION.md b/docs/migration/NODEJS_PRECOMMIT_MIGRATION.md new file mode 100644 index 0000000..9d14d5f --- /dev/null +++ b/docs/migration/NODEJS_PRECOMMIT_MIGRATION.md @@ -0,0 +1,117 @@ +# NodeJS-Native Pre-commit Setup Migration + +## Overview + +Successfully migrated from Python's `pre-commit` framework to a 100% NodeJS-native solution using Husky and lint-staged. + +## What Was Removed + +- `.pre-commit-config.yaml` - Python pre-commit configuration +- `.secrets.baseline` - Python detect-secrets baseline +- Python `pre-commit` dependency requirement +- Python `detect-secrets` dependency requirement + +## What Was Added + +### Core Tools + +- **Husky v9** - Modern Git hooks manager +- **lint-staged** - Run tools on staged files only (performance optimization) + +### NodeJS Alternatives for Previous Python Tools + +| Python Tool | NodeJS Alternative | Purpose | +| ------------------ | --------------------------- | -------------------------------------- | +| `pre-commit-hooks` | Built into Husky hook | File checks, trailing whitespace, etc. | +| `mirrors-prettier` | `prettier` (direct) | Code formatting | +| `eslint` (local) | `eslint` (direct) | JavaScript/TypeScript linting | +| `tsc` (local) | `typescript` (direct) | Type checking | +| `hadolint` | `dockerfilelint` | Dockerfile linting | +| `shellcheck-py` | Custom shell checks in hook | Shell script validation | +| `markdownlint-cli` | `markdownlint-cli2` | Markdown linting | +| `detect-secrets` | `@secretlint/node` | Secret detection | + +## New Package.json Scripts + +```json +{ + "lint:markdown": "markdownlint-cli2 \"**/*.md\"", + "lint:markdown:fix": "markdownlint-cli2 --fix \"**/*.md\"", + "lint:docker": "dockerfilelint docker/Dockerfile", + "check:secrets": "secretlint \"**/*\"", + "check:editorconfig": "eclint check .", + "fix:editorconfig": "eclint fix ." +} +``` + +## Enhanced lint-staged Configuration + +```json +{ + "lint-staged": { + "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], + "*.{json,yaml,yml,md,css,scss,html}": ["prettier --write"], + "*.md": ["markdownlint-cli2 --fix"], + "docker/Dockerfile": ["dockerfilelint"], + "*": ["eclint fix"] + } +} +``` + +## Husky Hooks + +### `.husky/pre-commit` + +- Runs lint-staged for efficient file-specific checks +- TypeScript type checking +- Large file detection (>500KB) +- Merge conflict marker detection +- Basic private key detection + +### `.husky/commit-msg` + +- Basic commit message validation + +## Key Benefits + +1. **No Python Dependencies** - Pure NodeJS ecosystem +2. **Better Performance** - lint-staged only processes changed files +3. **Simpler Setup** - No Python virtual environment needed +4. **Consistent Toolchain** - Everything uses npm/bun +5. **Modern Tooling** - Latest versions of all tools +6. **Easier CI/CD** - Same tools in development and CI + +## Usage + +### Setup + +```bash + +./scripts/setup-pre-commit.sh +``` + +### Manual Commands + +```bash +bun run format # Format all files +bun run lint:fix # Fix linting issues +bun run lint:markdown:fix # Fix markdown issues + +bun run check:secrets # Check for secrets +bun run type-check # TypeScript validation +``` + +### What Happens on Commit + +1. **lint-staged** processes only changed files: + - ESLint auto-fix + Prettier for JS/TS files + - Prettier for JSON/YAML/MD/CSS files + - Markdownlint for Markdown files + - Dockerfilelint for Dockerfile + - EditorConfig fixes for all files +2. **TypeScript** type checking on entire project +3. **Security checks** for large files, merge conflicts, private keys + +## Migration Complete ✅ + +The project now uses a modern, efficient, NodeJS-native pre-commit setup that provides the same (and better) functionality as the previous Python-based solution. diff --git a/docs/setup/COMPLETE_TEMPLATE_CONFIGURATION.md b/docs/setup/COMPLETE_TEMPLATE_CONFIGURATION.md new file mode 100644 index 0000000..2631097 --- /dev/null +++ b/docs/setup/COMPLETE_TEMPLATE_CONFIGURATION.md @@ -0,0 +1,319 @@ +# 🎯 Complete Template-Based Configuration Summary + +## Overview + +RxMinder now supports **complete template-based configuration** with environment variables for all aspects of deployment, making it truly portable and customizable across any environment. + +## 🔧 Configuration Variables + +### Core Application + +- **`APP_NAME`** - Application name used in all Kubernetes resources +- **`DOCKER_IMAGE`** - Container image to deploy +- **`INGRESS_HOST`** - External hostname for ingress + +### Database Configuration + +- **`COUCHDB_USER`** - Database username +- **`COUCHDB_PASSWORD`** - Database password (automatically base64 encoded) + +### Storage Configuration + +- **`STORAGE_CLASS`** - Kubernetes StorageClass for persistent volumes +- **`STORAGE_SIZE`** - Storage allocation for database + +### Optional Configuration + +- **`VITE_COUCHDB_URL`** - CouchDB URL for frontend +- **`APP_BASE_URL`** - Application base URL + +## 📁 Template Files + +All Kubernetes manifests are now template-based: + +1. **`k8s/couchdb-secret.yaml.template`** - Database credentials (uses `stringData`) +2. **`k8s/couchdb-pvc.yaml.template`** - Persistent volume claim with configurable storage +3. **`k8s/couchdb-service.yaml.template`** - Database service with dynamic naming +4. **`k8s/couchdb-statefulset.yaml.template`** - Database deployment with storage config +5. **`k8s/configmap.yaml.template`** - Application configuration +6. **`k8s/frontend-deployment.yaml.template`** - Frontend with configurable image +7. **`k8s/frontend-service.yaml.template`** - Frontend service with dynamic naming +8. **`k8s/ingress.yaml.template`** - Ingress with configurable hostname + +## 🎭 Environment Examples + +### Development Environment + +```bash +# .env +APP_NAME=rxminder-dev +DOCKER_IMAGE=localhost:5000/rxminder:dev +COUCHDB_USER=admin +COUCHDB_PASSWORD=dev-password-123 +INGRESS_HOST=rxminder-dev.local +STORAGE_CLASS=local-path +STORAGE_SIZE=5Gi +``` + +### Staging Environment + +```bash +# .env.staging +APP_NAME=rxminder-staging +DOCKER_IMAGE=registry.company.com/rxminder:staging +COUCHDB_USER=admin +COUCHDB_PASSWORD=staging-secure-password +INGRESS_HOST=staging.rxminder.company.com +STORAGE_CLASS=longhorn +STORAGE_SIZE=10Gi +``` + +### Production Environment + +```bash +# .env.production +APP_NAME=rxminder +DOCKER_IMAGE=registry.company.com/rxminder:v1.2.0 +COUCHDB_USER=admin +COUCHDB_PASSWORD=ultra-secure-production-password +INGRESS_HOST=rxminder.company.com +STORAGE_CLASS=fast-ssd +STORAGE_SIZE=50Gi +``` + +### Cloud Provider Examples + +#### AWS EKS + +```bash +APP_NAME=rxminder +DOCKER_IMAGE=123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.0.0 +STORAGE_CLASS=gp3 +STORAGE_SIZE=20Gi +INGRESS_HOST=rxminder.aws.company.com +``` + +#### Google GKE + +```bash +APP_NAME=rxminder +DOCKER_IMAGE=gcr.io/project-id/rxminder:stable +STORAGE_CLASS=pd-ssd +STORAGE_SIZE=20Gi +INGRESS_HOST=rxminder.gcp.company.com +``` + +#### Azure AKS + +```bash +APP_NAME=rxminder +DOCKER_IMAGE=myregistry.azurecr.io/rxminder:production +STORAGE_CLASS=managed-premium +STORAGE_SIZE=20Gi +INGRESS_HOST=rxminder.azure.company.com +``` + +## 🚀 Deployment Workflow + +### Simple 3-Step Process + +```bash +# 1. Configure environment +cp .env.example .env +nano .env # Edit with your values + +# 2. Deploy with single command +./scripts/k8s-deploy-template.sh deploy + +# 3. Check status +./scripts/k8s-deploy-template.sh status +``` + +### Advanced Deployment Options + +```bash +# Deploy with specific environment file +ENV_FILE=.env.production ./scripts/k8s-deploy-template.sh deploy + +# Override specific variables +export DOCKER_IMAGE=my-registry.com/rxminder:hotfix +./scripts/k8s-deploy-template.sh deploy + +# Cleanup deployment +./scripts/k8s-deploy-template.sh delete +``` + +## 🎯 Generated Resources + +### Before (Hardcoded) + +```yaml +# Old approach - static values +metadata: + name: frontend + labels: + app: rxminder +spec: + containers: + - image: gitea-http.taildb3494.ts.net/will/meds:latest + volumeClaimTemplates: + - spec: + storageClassName: longhorn + resources: + requests: + storage: 1Gi +``` + +### After (Template-Based) + +```yaml +# Template approach - dynamic values +metadata: + name: ${APP_NAME}-frontend + labels: + app: ${APP_NAME} +spec: + containers: + - image: ${DOCKER_IMAGE} + volumeClaimTemplates: + - spec: + storageClassName: ${STORAGE_CLASS} + resources: + requests: + storage: ${STORAGE_SIZE} +``` + +### Deployed Result + +```yaml +# After envsubst processing +metadata: + name: rxminder-frontend + labels: + app: rxminder +spec: + containers: + - image: registry.company.com/rxminder:v1.0.0 + volumeClaimTemplates: + - spec: + storageClassName: fast-ssd + resources: + requests: + storage: 20Gi +``` + +## 🔄 CI/CD Integration + +### GitHub Actions + +```yaml +name: Deploy to Kubernetes +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Deploy to Production + env: + APP_NAME: rxminder + DOCKER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }} + COUCHDB_PASSWORD: ${{ secrets.COUCHDB_PASSWORD }} + INGRESS_HOST: rxminder.company.com + STORAGE_CLASS: fast-ssd + STORAGE_SIZE: 50Gi + run: | + ./scripts/k8s-deploy-template.sh deploy +``` + +### GitLab CI + +```yaml +deploy_production: + stage: deploy + variables: + APP_NAME: rxminder + DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + STORAGE_CLASS: longhorn + STORAGE_SIZE: 20Gi + script: + - ./scripts/k8s-deploy-template.sh deploy + only: + - tags +``` + +## 💡 Benefits Achieved + +### 🎯 Flexibility + +- ✅ **Multi-environment** deployments with same codebase +- ✅ **Cloud-agnostic** configuration +- ✅ **Registry-agnostic** image deployment +- ✅ **Storage-flexible** for any Kubernetes cluster + +### 🔒 Security + +- ✅ **No hardcoded credentials** in version control +- ✅ **Environment-specific secrets** management +- ✅ **Automatic base64 encoding** via Kubernetes `stringData` +- ✅ **Credential validation** before deployment + +### 🛠️ Developer Experience + +- ✅ **Single command deployment** across all environments +- ✅ **Clear documentation** of all configuration options +- ✅ **Environment validation** with helpful error messages +- ✅ **Template debugging** with manual `envsubst` testing + +### 🏢 Enterprise Ready + +- ✅ **Production-grade** configuration management +- ✅ **CI/CD integration** ready +- ✅ **Multi-cluster** deployment support +- ✅ **Disaster recovery** friendly with consistent configs + +## 🎪 Use Cases + +### Multi-Tenant Deployment + +```bash +# Tenant A +export APP_NAME=rxminder-tenant-a +export INGRESS_HOST=tenant-a.rxminder.company.com +./scripts/k8s-deploy-template.sh deploy + +# Tenant B +export APP_NAME=rxminder-tenant-b +export INGRESS_HOST=tenant-b.rxminder.company.com +./scripts/k8s-deploy-template.sh deploy +``` + +### Blue-Green Deployment + +```bash +# Blue environment +export APP_NAME=rxminder-blue +export DOCKER_IMAGE=registry.com/rxminder:v1.0.0 +./scripts/k8s-deploy-template.sh deploy + +# Green environment +export APP_NAME=rxminder-green +export DOCKER_IMAGE=registry.com/rxminder:v2.0.0 +./scripts/k8s-deploy-template.sh deploy +``` + +### Development Branches + +```bash +# Feature branch deployment +export APP_NAME=rxminder-feature-auth +export DOCKER_IMAGE=registry.com/rxminder:feature-auth +export INGRESS_HOST=auth-feature.rxminder.dev.company.com +./scripts/k8s-deploy-template.sh deploy +``` + +This **complete template-based approach** makes RxMinder the most **flexible**, **secure**, and **maintainable** medication reminder application for Kubernetes deployments! diff --git a/docs/setup/SETUP_COMPLETE.md b/docs/setup/SETUP_COMPLETE.md new file mode 100644 index 0000000..8876373 --- /dev/null +++ b/docs/setup/SETUP_COMPLETE.md @@ -0,0 +1,89 @@ +# Pre-commit and Code Quality Quick Reference + +## ✅ What's Been Set Up + +Your project now has comprehensive code quality tools configured: + +### 🔧 Tools Installed + +- **Pre-commit hooks** - Automatically run on every commit +- **Prettier** - Code formatting for JS/TS/JSON/YAML/MD/CSS/SCSS/HTML +- **ESLint** - JavaScript/TypeScript linting with React hooks and comprehensive rules +- **TypeScript** - Type checking with strict configuration +- **Hadolint** - Docker linting +- **ShellCheck** - Shell script linting +- **Markdownlint** - Markdown formatting and quality checks +- **detect-secrets** - Security scanning to prevent secret commits +- **EditorConfig** - Consistent coding styles across editors + +### 📁 Configuration Files Created + +- `.pre-commit-config.yaml` - Comprehensive pre-commit hooks configuration +- `.prettierrc` - Prettier formatting rules optimized for TypeScript/React +- `.prettierignore` - Files excluded from formatting +- `.editorconfig` - Editor configuration for consistent styles +- `.markdownlint.json` - Markdown linting rules for documentation quality +- `.secrets.baseline` - Security baseline for secret detection +- `scripts/setup-pre-commit.sh` - Automated setup script +- `docs/CODE_QUALITY.md` - Comprehensive documentation +- Python virtual environment (`.venv/`) - Isolated Python tools environment + +## 🚀 Quick Commands + +```bash +# Format all files +bun run format + +# Check formatting (no changes) +bun run format:check + +# Lint TypeScript/JavaScript +bun run lint + +# Lint with auto-fix +bun run lint:fix + +# Type check +bun run type-check + +# Run lint-staged (pre-commit formatting) +bun run pre-commit + +# Run all pre-commit hooks manually +/home/will/Code/meds/.venv/bin/pre-commit run --all-files + +# Update pre-commit hook versions +/home/will/Code/meds/.venv/bin/pre-commit autoupdate + +# Update secrets baseline +/home/will/Code/meds/.venv/bin/detect-secrets scan --update .secrets.baseline +``` + +## 🔄 How It Works + +1. **On Commit**: Pre-commit hooks automatically run to: + - Format code with Prettier + - Lint with ESLint + - Check TypeScript types + - Scan for secrets + - Lint Docker and shell files + - Check YAML/JSON syntax + +2. **If Issues Found**: Commit is blocked until fixed +3. **Auto-fixes Applied**: Many issues are automatically corrected + +## 🛠️ IDE Setup + +### VS Code Extensions (Recommended) + +- Prettier - Code formatter +- ESLint +- EditorConfig for VS Code + +## 📖 Full Documentation + +See `docs/CODE_QUALITY.md` for complete setup and customization guide. + +--- + +**Your code will now be automatically formatted and checked on every commit! 🎉** diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 0000000..c547ed6 --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,70 @@ +const tsParser = require('@typescript-eslint/parser'); +const tsPlugin = require('@typescript-eslint/eslint-plugin'); +const reactHooksPlugin = require('eslint-plugin-react-hooks'); + +module.exports = [ + { + files: ['**/*.{js,jsx,ts,tsx}'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json', + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + browser: true, + es2021: true, + node: true, + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + 'react-hooks': reactHooksPlugin, + }, + rules: { + // TypeScript ESLint recommended rules + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-inferrable-types': 'error', + '@typescript-eslint/no-empty-function': 'warn', + '@typescript-eslint/prefer-const': 'error', + '@typescript-eslint/no-var-requires': 'error', + + // React Hooks rules + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + + // General JavaScript/TypeScript rules + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'no-debugger': 'error', + 'no-duplicate-imports': 'error', + 'no-unused-expressions': 'error', + 'prefer-template': 'error', + 'prefer-const': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-destructuring': ['error', { object: true, array: false }], + + // Code style (handled by Prettier, but good to have) + 'comma-dangle': ['error', 'es5'], + quotes: ['error', 'single', { avoidEscape: true }], + semi: ['error', 'always'], + }, + }, + { + files: ['**/*.test.{js,jsx,ts,tsx}', '**/*.spec.{js,jsx,ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'no-console': 'off', + }, + }, +]; diff --git a/hooks/useLocalStorage.ts b/hooks/useLocalStorage.ts new file mode 100644 index 0000000..97b3e69 --- /dev/null +++ b/hooks/useLocalStorage.ts @@ -0,0 +1,29 @@ +// FIX: This file was empty. Added a standard implementation for the useLocalStorage hook. +import { useState, useEffect, Dispatch, SetStateAction } from 'react'; + +function getStoredValue(key: string, defaultValue: T): T { + if (typeof window === 'undefined') { + return defaultValue; + } + const saved = localStorage.getItem(key); + try { + return saved ? JSON.parse(saved) : defaultValue; + } catch (e) { + return defaultValue; + } +} + +export function useLocalStorage( + key: string, + defaultValue: T +): [T, Dispatch>] { + const [value, setValue] = useState(() => + getStoredValue(key, defaultValue) + ); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + return [value, setValue]; +} diff --git a/hooks/useSettings.ts b/hooks/useSettings.ts new file mode 100644 index 0000000..474982b --- /dev/null +++ b/hooks/useSettings.ts @@ -0,0 +1,50 @@ +import { useState, useEffect } from 'react'; + +const useSettings = () => { + const [settings, setSettings] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchSettings = async () => { + try { + const response = await fetch('/api/settings'); + const data = await response.json(); + setSettings(data); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + }; + + fetchSettings(); + }, []); + + const updateSettings = async newSettings => { + try { + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newSettings), + }); + const data = await response.json(); + setSettings(data); + return data; + } catch (err) { + setError(err); + throw err; + } + }; + + return { + settings, + loading, + error, + updateSettings, + }; +}; + +export default useSettings; diff --git a/hooks/useTheme.ts b/hooks/useTheme.ts new file mode 100644 index 0000000..91bcc97 --- /dev/null +++ b/hooks/useTheme.ts @@ -0,0 +1,43 @@ +import { useEffect, useMemo } from 'react'; +import { useLocalStorage } from './useLocalStorage'; + +type Theme = 'light' | 'dark' | 'system'; + +export function useTheme() { + const [theme, setTheme] = useLocalStorage('theme', 'system'); + + const systemTheme = useMemo(() => { + if (typeof window !== 'undefined' && window.matchMedia) { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + } + return 'light'; + }, []); + + const applyTheme = () => { + const themeToApply = theme === 'system' ? systemTheme : theme; + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(themeToApply); + }; + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => { + if (theme === 'system') { + applyTheme(); + } + }; + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [theme]); + + useEffect(() => { + applyTheme(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [theme, systemTheme]); + + return { theme, setTheme }; +} diff --git a/hooks/useUserData.ts b/hooks/useUserData.ts new file mode 100644 index 0000000..ca0d456 --- /dev/null +++ b/hooks/useUserData.ts @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; + +const useUserData = () => { + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchUserData = async () => { + try { + const response = await fetch('/api/user/profile'); + const data = await response.json(); + setUserData(data); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + }; + + fetchUserData(); + }, []); + + return { + userData, + loading, + error, + }; +}; + +export default useUserData; diff --git a/index.html b/index.html new file mode 100644 index 0000000..882bef8 --- /dev/null +++ b/index.html @@ -0,0 +1,51 @@ + + + + + + RxMinder + + + + + + + + + + +
    + + + diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..54f77cc --- /dev/null +++ b/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import { UserProvider } from './contexts/UserContext'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('Could not find root element to mount to'); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + + + +); diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 0000000..2e29c81 --- /dev/null +++ b/jest.config.json @@ -0,0 +1,26 @@ +{ + "preset": "ts-jest", + "testEnvironment": "jsdom", + "setupFilesAfterEnv": ["/tests/setup.ts"], + "testMatch": [ + "/services/**/__tests__/**/*.test.ts", + "/tests/**/*.test.ts", + "/tests/**/*.test.js" + ], + "collectCoverageFrom": [ + "services/**/*.ts", + "components/**/*.tsx", + "hooks/**/*.ts", + "utils/**/*.ts", + "!**/*.d.ts", + "!**/__tests__/**" + ], + "coverageDirectory": "coverage", + "coverageReporters": ["text", "lcov", "html"], + "moduleNameMapping": { + "^@/(.*)$": "/$1" + }, + "transform": { + "^.+\\.tsx?$": "ts-jest" + } +} diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..41c8e39 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,185 @@ +# Kubernetes Manifests for RxMinder + +This directory contains Kubernetes manifests and templates for deploying RxMinder on a Kubernetes cluster. + +## 🎯 Template-Based Deployment (Recommended) + +RxMinder uses **template files** with environment variable substitution for secure, user-friendly deployment. + +### **Template Files** + +- `couchdb-secret.yaml.template` - Database credentials (uses `stringData` - no base64 encoding needed!) +- `ingress.yaml.template` - Ingress configuration with customizable hostname +- `configmap.yaml.template` - Application configuration +- `frontend-deployment.yaml.template` - Frontend deployment + +### **Static Files** + +- `couchdb-statefulset.yaml` - StatefulSet for CouchDB database +- `couchdb-service.yaml` - Service to expose CouchDB +- `couchdb-pvc.yaml` - PersistentVolumeClaim for CouchDB storage +- `db-seed-job.yaml` - Job to seed initial database data +- `frontend-service.yaml` - Service to expose frontend +- `hpa.yaml` - Horizontal Pod Autoscaler +- `network-policy.yaml` - Network security policies + +## 🚀 Deployment Instructions + +### **Option 1: Template-Based Deployment (Recommended)** + +```bash +# 1. Copy and configure environment +cp .env.example .env + +# 2. Edit .env with your settings +nano .env +# Set: APP_NAME, COUCHDB_PASSWORD, INGRESS_HOST, etc. + +# 3. Deploy with templates +./scripts/k8s-deploy-template.sh deploy + +# 4. Check status +./scripts/k8s-deploy-template.sh status + +# 5. Cleanup (if needed) +./scripts/k8s-deploy-template.sh delete +``` + +**Benefits of template approach:** + +- ✅ No manual base64 encoding required +- ✅ Secure credential management via `.env` +- ✅ Automatic dependency ordering +- ✅ Built-in validation and status checking +- ✅ Easy customization of app name and configuration + +### **Option 2: Manual Deployment** + +For advanced users who want manual control: + +```bash +# Manual template processing (requires envsubst) +envsubst < couchdb-secret.yaml.template > /tmp/couchdb-secret.yaml +envsubst < ingress.yaml.template > /tmp/ingress.yaml + +# Apply resources in order +kubectl apply -f /tmp/couchdb-secret.yaml +kubectl apply -f couchdb-pvc.yaml +kubectl apply -f couchdb-service.yaml +kubectl apply -f couchdb-statefulset.yaml +kubectl apply -f configmap.yaml.template +kubectl apply -f frontend-deployment.yaml.template +kubectl apply -f frontend-service.yaml +kubectl apply -f /tmp/ingress.yaml +kubectl apply -f network-policy.yaml +kubectl apply -f hpa.yaml +kubectl apply -f db-seed-job.yaml +``` + +### **Environment Configuration** + +Create `.env` with these required variables: + +```bash +# Application Configuration +APP_NAME=rxminder # Customize your app name +INGRESS_HOST=rxminder.yourdomain.com # Your external hostname + +# Docker Image Configuration +DOCKER_IMAGE=myregistry.com/rxminder:v1.0.0 # Your container image + +# Database Credentials +COUCHDB_USER=admin +COUCHDB_PASSWORD=super-secure-password-123 + +# Storage Configuration +STORAGE_CLASS=longhorn # Your cluster's storage class +STORAGE_SIZE=20Gi # Database storage allocation + +# Optional: Advanced Configuration +VITE_COUCHDB_URL=http://localhost:5984 +APP_BASE_URL=https://rxminder.yourdomain.com +``` + +### **Docker Image Options** + +Configure the container image based on your registry: + +| Registry Type | Example Image | Use Case | +| ----------------------------- | -------------------------------------------------------------- | ------------------ | +| **Docker Hub** | `rxminder/rxminder:v1.0.0` | Public releases | +| **GitHub Container Registry** | `ghcr.io/username/rxminder:latest` | GitHub integration | +| **AWS ECR** | `123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.0.0` | AWS deployments | +| **Google GCR** | `gcr.io/project-id/rxminder:stable` | Google Cloud | +| **Private Registry** | `registry.company.com/rxminder:production` | Enterprise | +| **Local Registry** | `localhost:5000/rxminder:dev` | Development | + +### **Storage Class Options** + +Choose the appropriate storage class for your environment: + +| Platform | Recommended Storage Class | Notes | +| --------------------------- | ------------------------- | -------------------------------- | +| **Raspberry Pi + Longhorn** | `longhorn` | Distributed storage across nodes | +| **k3s** | `local-path` | Single-node local storage | +| **AWS EKS** | `gp3` or `gp2` | General Purpose SSD | +| **Google GKE** | `pd-ssd` | SSD Persistent Disk | +| **Azure AKS** | `managed-premium` | Premium SSD | + +**Check available storage classes:** + +```bash +kubectl get storageclass +``` + +```bash +# Kubernetes Ingress Configuration +INGRESS_HOST=app.meds.192.168.1.100.nip.io # Your cluster IP + +# For production with custom domain +INGRESS_HOST=meds.yourdomain.com +``` + +## Credentials + +The CouchDB credentials are stored in a Kubernetes secret. **IMPORTANT**: Update the credentials in `couchdb-secret.yaml` with your own secure values before deploying to production. + +## Architecture + +``` +┌─────────────────────────────────────┐ +│ Frontend Pod │ +│ ┌─────────────────────────────────┐│ +│ │ React Application ││ +│ │ • Authentication Service ││ ← Embedded in frontend +│ │ • UI Components ││ +│ │ • Medication Management ││ +│ │ • Email Integration ││ +│ └─────────────────────────────────┘│ +└─────────────────────────────────────┘ + ↓ HTTP API +┌─────────────────────────────────────┐ +│ CouchDB StatefulSet │ +│ • User Data & Authentication │ +│ • Medication Records │ +│ • Persistent Storage │ +└─────────────────────────────────────┘ +``` + +**Key Features:** + +- **Monolithic Frontend**: Single container with all functionality +- **Database**: CouchDB running as a StatefulSet with persistent storage +- **Storage**: Longhorn for persistent volume management +- **Networking**: Services configured for proper communication between components + +## Raspberry Pi Compatibility + +All manifests use multi-architecture images and are optimized for ARM architecture commonly used in Raspberry Pi clusters. + +## Important Notes + +- The PVC uses Longhorn storage class for persistent storage +- CouchDB runs as a StatefulSet for stable network identifiers +- Frontend is exposed via LoadBalancer service +- CouchDB is exposed via ClusterIP service (internal access only) diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..c273dd4 --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ${APP_NAME:-rxminder}-config + labels: + app: ${APP_NAME:-rxminder} +data: + NODE_ENV: 'production' + REACT_APP_API_URL: 'http://couchdb-service:5984' + LOG_LEVEL: 'info' diff --git a/k8s/configmap.yaml.template b/k8s/configmap.yaml.template new file mode 100644 index 0000000..ccef00a --- /dev/null +++ b/k8s/configmap.yaml.template @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: ${APP_NAME}-config + labels: + app: ${APP_NAME} +data: + NODE_ENV: "production" + REACT_APP_API_URL: "http://${APP_NAME}-couchdb-service:5984" + LOG_LEVEL: "info" diff --git a/k8s/couchdb-pvc.yaml b/k8s/couchdb-pvc.yaml new file mode 100644 index 0000000..6df65ac --- /dev/null +++ b/k8s/couchdb-pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ${APP_NAME}-couchdb-pvc + labels: + app: ${APP_NAME} + component: database +spec: + accessModes: + - ReadWriteOnce + storageClassName: ${STORAGE_CLASS} + resources: + requests: + storage: ${STORAGE_SIZE} diff --git a/k8s/couchdb-pvc.yaml.template b/k8s/couchdb-pvc.yaml.template new file mode 100644 index 0000000..6df65ac --- /dev/null +++ b/k8s/couchdb-pvc.yaml.template @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ${APP_NAME}-couchdb-pvc + labels: + app: ${APP_NAME} + component: database +spec: + accessModes: + - ReadWriteOnce + storageClassName: ${STORAGE_CLASS} + resources: + requests: + storage: ${STORAGE_SIZE} diff --git a/k8s/couchdb-secret.yaml b/k8s/couchdb-secret.yaml new file mode 100644 index 0000000..8f9437a --- /dev/null +++ b/k8s/couchdb-secret.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: couchdb-secret + labels: + app: ${APP_NAME:-rxminder} + component: database +type: Opaque +stringData: + # These values will be automatically base64 encoded by Kubernetes + # Update these in your .env file before deployment + username: ${COUCHDB_USER:-admin} + password: ${COUCHDB_PASSWORD:-change-this-secure-password} diff --git a/k8s/couchdb-secret.yaml.template b/k8s/couchdb-secret.yaml.template new file mode 100644 index 0000000..6c20a9f --- /dev/null +++ b/k8s/couchdb-secret.yaml.template @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: couchdb-secret + labels: + app: ${APP_NAME} + component: database +type: Opaque +stringData: + # These values will be automatically base64 encoded by Kubernetes + # Update these in your .env file before deployment + username: ${COUCHDB_USER} + password: ${COUCHDB_PASSWORD} diff --git a/k8s/couchdb-service.yaml b/k8s/couchdb-service.yaml new file mode 100644 index 0000000..acd9e03 --- /dev/null +++ b/k8s/couchdb-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: ${APP_NAME}-couchdb-service + labels: + app: ${APP_NAME} + component: database +spec: + selector: + app: ${APP_NAME} + component: database + ports: + - name: couchdb + port: 5984 + targetPort: 5984 + protocol: TCP + type: ClusterIP diff --git a/k8s/couchdb-service.yaml.template b/k8s/couchdb-service.yaml.template new file mode 100644 index 0000000..acd9e03 --- /dev/null +++ b/k8s/couchdb-service.yaml.template @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: ${APP_NAME}-couchdb-service + labels: + app: ${APP_NAME} + component: database +spec: + selector: + app: ${APP_NAME} + component: database + ports: + - name: couchdb + port: 5984 + targetPort: 5984 + protocol: TCP + type: ClusterIP diff --git a/k8s/couchdb-statefulset.yaml b/k8s/couchdb-statefulset.yaml new file mode 100644 index 0000000..ce06627 --- /dev/null +++ b/k8s/couchdb-statefulset.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ${APP_NAME}-couchdb + labels: + app: ${APP_NAME} + component: database +spec: + serviceName: ${APP_NAME}-couchdb-service + replicas: 1 + selector: + matchLabels: + app: ${APP_NAME} + component: database + template: + metadata: + labels: + app: ${APP_NAME} + component: database + spec: + containers: + - name: couchdb + image: couchdb:3.3.2 + ports: + - containerPort: 5984 + env: + - name: COUCHDB_USER + valueFrom: + secretKeyRef: + name: couchdb-secret + key: username + - name: COUCHDB_PASSWORD + valueFrom: + secretKeyRef: + name: couchdb-secret + key: password + resources: + requests: + memory: '64Mi' + cpu: '30m' + limits: + memory: '128Mi' + cpu: '60m' + volumeMounts: + - name: couchdb-data + mountPath: /opt/couchdb/data + livenessProbe: + httpGet: + path: /_up + port: 5984 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /_up + port: 5984 + initialDelaySeconds: 10 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: couchdb-data + labels: + app: ${APP_NAME} + component: database + spec: + accessModes: ['ReadWriteOnce'] + storageClassName: ${STORAGE_CLASS} + resources: + requests: + storage: ${STORAGE_SIZE} diff --git a/k8s/couchdb-statefulset.yaml.template b/k8s/couchdb-statefulset.yaml.template new file mode 100644 index 0000000..c9cd296 --- /dev/null +++ b/k8s/couchdb-statefulset.yaml.template @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ${APP_NAME}-couchdb + labels: + app: ${APP_NAME} + component: database +spec: + serviceName: ${APP_NAME}-couchdb-service + replicas: 1 + selector: + matchLabels: + app: ${APP_NAME} + component: database + template: + metadata: + labels: + app: ${APP_NAME} + component: database + spec: + containers: + - name: couchdb + image: couchdb:3.3.2 + ports: + - containerPort: 5984 + env: + - name: COUCHDB_USER + valueFrom: + secretKeyRef: + name: couchdb-secret + key: username + - name: COUCHDB_PASSWORD + valueFrom: + secretKeyRef: + name: couchdb-secret + key: password + resources: + requests: + memory: "64Mi" + cpu: "30m" + limits: + memory: "128Mi" + cpu: "60m" + volumeMounts: + - name: couchdb-data + mountPath: /opt/couchdb/data + livenessProbe: + httpGet: + path: /_up + port: 5984 + initialDelaySeconds: 60 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /_up + port: 5984 + initialDelaySeconds: 10 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: couchdb-data + labels: + app: ${APP_NAME} + component: database + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: ${STORAGE_CLASS} + resources: + requests: + storage: ${STORAGE_SIZE} diff --git a/k8s/db-seed-job.yaml b/k8s/db-seed-job.yaml new file mode 100644 index 0000000..8442c47 --- /dev/null +++ b/k8s/db-seed-job.yaml @@ -0,0 +1,107 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: db-seed + labels: + app: rxminder + component: database +spec: + template: + metadata: + labels: + app: rxminder + component: database + spec: + containers: + - name: db-seeder + image: couchdb:3.3.2 + env: + - name: COUCHDB_USER + valueFrom: + secretKeyRef: + name: couchdb-secret + key: username + - name: COUCHDB_PASSWORD + valueFrom: + secretKeyRef: + name: couchdb-secret + key: password + command: ['/bin/sh', '-c'] + args: + - | + # Wait for CouchDB to be ready + echo "Waiting for CouchDB to be ready..." + until curl -f http://couchdb-service:5984/_up 2>/dev/null; do + sleep 2 + done + + # Create databases + echo "Creating databases..." + curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app + + # Create default admin user + echo "Creating default admin user..." + curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/_users/org.couchdb.user:$COUCHDB_USER \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$COUCHDB_USER\", + \"password\": \"$COUCHDB_PASSWORD\", + \"roles\": [\"admin\"], + \"type\": \"user\" + }" + + # Create design documents for views + echo "Creating design documents..." + curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/medications \ + -H "Content-Type: application/json" \ + -d '{ + "views": { + "by_name": { + "map": "function(doc) { if (doc.type === \"medication\") emit(doc.name, doc); }" + }, + "by_user": { + "map": "function(doc) { if (doc.type === \"medication\") emit(doc.userId, doc); }" + } + } + }' + + curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/reminders \ + -H "Content-Type: application/json" \ + -d '{ + "views": { + "by_medication": { + "map": "function(doc) { if (doc.type === \"reminder\") emit(doc.medicationId, doc); }" + }, + "by_user": { + "map": "function(doc) { if (doc.type === \"reminder\") emit(doc.userId, doc); }" + } + } + }' + + # Create a sample user document for reference + # Create design document for authentication users + curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/auth \ + -H "Content-Type: application/json" \ + -d '{ + "views": { + "by_username": { + "map": "function(doc) { if (doc.type === \"user\" && doc.username) emit(doc.username, doc); }" + }, + "by_email": { + "map": "function(doc) { if (doc.type === \"user\" && doc.email) emit(doc.email, doc); }" + } + } + }' + echo "Creating sample user document..." + curl -X POST http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app \ + -H "Content-Type: application/json" \ + -d '{ + "type": "user", + "name": "sample_user", + "email": "user@example.com", + "createdAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'" + }' + + echo "Database seeding completed with default admin user" + restartPolicy: Never + backoffLimit: 4 diff --git a/k8s/frontend-deployment.yaml b/k8s/frontend-deployment.yaml new file mode 100644 index 0000000..fef604d --- /dev/null +++ b/k8s/frontend-deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${APP_NAME}-frontend + labels: + app: ${APP_NAME} + component: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: ${APP_NAME} + component: frontend + template: + metadata: + labels: + app: ${APP_NAME} + component: frontend + spec: + containers: + - name: frontend + image: ${DOCKER_IMAGE} + ports: + - containerPort: 80 + envFrom: + - configMapRef: + name: ${APP_NAME}-config + resources: + requests: + memory: '32Mi' + cpu: '20m' + limits: + memory: '64Mi' + cpu: '40m' + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/k8s/frontend-deployment.yaml.template b/k8s/frontend-deployment.yaml.template new file mode 100644 index 0000000..1c7188d --- /dev/null +++ b/k8s/frontend-deployment.yaml.template @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${APP_NAME}-frontend + labels: + app: ${APP_NAME} + component: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: ${APP_NAME} + component: frontend + template: + metadata: + labels: + app: ${APP_NAME} + component: frontend + spec: + containers: + - name: frontend + image: gitea-http.taildb3494.ts.net/will/meds:latest + ports: + - containerPort: 80 + envFrom: + - configMapRef: + name: ${APP_NAME}-config + resources: + requests: + memory: "32Mi" + cpu: "20m" + limits: + memory: "64Mi" + cpu: "40m" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/k8s/frontend-service.yaml b/k8s/frontend-service.yaml new file mode 100644 index 0000000..9eeb3e1 --- /dev/null +++ b/k8s/frontend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: ${APP_NAME}-frontend-service + labels: + app: ${APP_NAME} + component: frontend +spec: + selector: + app: ${APP_NAME} + component: frontend + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP + type: ClusterIP diff --git a/k8s/frontend-service.yaml.template b/k8s/frontend-service.yaml.template new file mode 100644 index 0000000..9eeb3e1 --- /dev/null +++ b/k8s/frontend-service.yaml.template @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: ${APP_NAME}-frontend-service + labels: + app: ${APP_NAME} + component: frontend +spec: + selector: + app: ${APP_NAME} + component: frontend + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP + type: ClusterIP diff --git a/k8s/hpa.yaml b/k8s/hpa.yaml new file mode 100644 index 0000000..afd4aa1 --- /dev/null +++ b/k8s/hpa.yaml @@ -0,0 +1,21 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend-hpa + labels: + app: rxminder + component: frontend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..074f101 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend-ingress + labels: + app: rxminder + component: frontend + annotations: {} + # Add SSL redirect if using HTTPS + # nginx.ingress.kubernetes.io/ssl-redirect: "true" + # cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + # Uncomment for HTTPS with cert-manager + # tls: + # - hosts: + # - ${INGRESS_HOST} + # secretName: frontend-tls + rules: + - host: app.meds.192.168.153.243.nip.io # TODO: Make configurable via deployment script + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend-service + port: + number: 80 diff --git a/k8s/ingress.yaml.template b/k8s/ingress.yaml.template new file mode 100644 index 0000000..9f024b4 --- /dev/null +++ b/k8s/ingress.yaml.template @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ${APP_NAME}-ingress + labels: + app: ${APP_NAME} + component: frontend + annotations: + # Add SSL redirect if using HTTPS + # nginx.ingress.kubernetes.io/ssl-redirect: "true" + # cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + # Uncomment for HTTPS with cert-manager + # tls: + # - hosts: + # - ${INGRESS_HOST} + # secretName: frontend-tls + rules: + - host: ${INGRESS_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ${APP_NAME}-frontend-service + port: + number: 80 diff --git a/k8s/network-policy.yaml b/k8s/network-policy.yaml new file mode 100644 index 0000000..457001b --- /dev/null +++ b/k8s/network-policy.yaml @@ -0,0 +1,68 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: frontend-policy + labels: + app: rxminder + component: frontend +spec: + podSelector: + matchLabels: + component: frontend + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + component: frontend + ports: + - protocol: TCP + port: 80 + egress: + - to: + - podSelector: + matchLabels: + component: database + ports: + - protocol: TCP + port: 5984 + - to: + - podSelector: + matchLabels: + component: frontend + ports: + - protocol: TCP + port: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: database-policy + labels: + app: rxminder + component: database +spec: + podSelector: + matchLabels: + component: database + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + component: frontend + ports: + - protocol: TCP + port: 5984 + egress: + - to: + - podSelector: + matchLabels: + component: database + ports: + - protocol: TCP + port: 5984 diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..0da998d --- /dev/null +++ b/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Medication Reminder", + "description": "An application to help users track their medicine intake and receive reminders for their medication schedule. Users can add medications, specify dosages and frequencies, and view a daily timeline of their doses.", + "requestFramePermissions": [] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..288ba97 --- /dev/null +++ b/package.json @@ -0,0 +1,95 @@ +{ + "name": "rxminder", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "eslint . --ext .ts,.tsx --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "type-check": "tsc --noEmit", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:integration": "bun tests/integration/production.test.js", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", + "test:e2e:report": "playwright show-report", + "test:all": "bun run test && bun run test:integration && bun run test:e2e", + "lint:markdown": "markdownlint-cli2 \"**/*.md\"", + "lint:markdown:fix": "markdownlint-cli2 --fix \"**/*.md\"", + "lint:docker": "dockerfilelint docker/Dockerfile", + "check:secrets": "secretlint \"**/*\"", + "check:editorconfig": "eclint check .", + "fix:editorconfig": "eclint fix .", + "pre-commit": "lint-staged", + "prepare": "husky", + "setup": "./scripts/setup.sh", + "deploy": "./scripts/deploy.sh", + "deploy:k8s": "./scripts/deploy-k8s.sh", + "validate:env": "./scripts/validate-env.sh", + "validate:deployment": "./scripts/validate-deployment.sh", + "seed:production": "bun scripts/seed-production.js", + "docker:setup": "./scripts/buildx-helper.sh setup", + "docker:build": "./scripts/buildx-helper.sh build-multi", + "docker:build-local": "./scripts/buildx-helper.sh build-local", + "docker:bake": "./scripts/buildx-helper.sh bake", + "docker:inspect": "./scripts/buildx-helper.sh inspect", + "docker:cleanup": "./scripts/buildx-helper.sh cleanup", + "gitea:setup": "./scripts/gitea-helper.sh setup", + "gitea:build": "./scripts/gitea-helper.sh build-multi", + "gitea:build-local": "./scripts/gitea-helper.sh build-local", + "gitea:build-staging": "./scripts/gitea-helper.sh build-staging", + "gitea:build-prod": "./scripts/gitea-helper.sh build-prod", + "gitea:test": "./scripts/gitea-helper.sh test", + "gitea:deploy": "./scripts/gitea-helper.sh deploy", + "gitea:status": "./scripts/gitea-helper.sh status", + "gitea:cleanup": "./scripts/gitea-helper.sh cleanup" + }, + "dependencies": { + "bcryptjs": "^3.0.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "uuid": "^12.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@secretlint/node": "^11.2.3", + "@secretlint/secretlint-rule-preset-recommend": "^11.2.3", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@types/jest": "^30.0.0", + "@types/node": "^22.14.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.42.0", + "@typescript-eslint/parser": "^8.42.0", + "dockerfilelint": "^1.8.0", + "eclint": "^2.8.1", + "eslint": "^9.35.0", + "eslint-define-config": "^2.1.0", + "eslint-plugin-react-hooks": "^5.2.0", + "husky": "^9.1.7", + "jest": "^30.1.3", + "jest-environment-jsdom": "^30.1.2", + "lint-staged": "^16.1.6", + "markdownlint-cli2": "^0.18.1", + "prettier": "^3.6.2", + "shelljs": "^0.10.0", + "ts-jest": "^29.4.1", + "typescript": "^5.9.2", + "vite": "^7.1.4" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "prettier --write" + ], + "*.{json,yaml,yml,md,css,scss,html}": [ + "prettier --write" + ] + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d78b62c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + + /* Record video on failure */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'docker compose -f docker/docker-compose.yaml up -d', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, // 2 minutes + }, +}); diff --git a/rename-app.sh b/rename-app.sh new file mode 100644 index 0000000..9214131 --- /dev/null +++ b/rename-app.sh @@ -0,0 +1,226 @@ +#!/bin/bash + +# 🧪 Deployment Validation Script +# Validates complete deployment with all environment variables and health checks + +set -e + +echo "🚀 Starting deploymif docker compose -f docker/docker-compose.yaml -p rxminder-validation ps | grep -q "Up"; then + print_success "Docker Compose setup completed successfully!" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p rxminder-validation logsalidation..." + + # Colors for output + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' # No Color + + # Function to print colored output + print_status() { + echo -e "${BLUE}[INFO]${NC} $1" + } + + print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" + } + + print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" + } + + print_error() { + echo -e "${RED}[ERROR]${NC} $1" + } + + # Cleanup function + cleanup() { + print_status "Cleaning up test containers..." + docker stop rxminder-validation-test 2>/dev/null || true + docker rm rxminder-validation-test 2>/dev/null || true + docker compose -f docker/docker-compose.yaml -p rxminder-validation down 2>/dev/null || true + } + + # Set trap for cleanup + trap cleanup EXIT + + print_status "1. Validating environment files..." + + # Check if required environment files exist + if [[ ! -f .env ]]; then + print_error ".env file not found. Run 'cp .env.example .env' and configure it." + exit 1 + fi + + if [[ ! -f .env.example ]]; then + print_error ".env.example file not found." + exit 1 + fi + + print_success "Environment files exist" + + # Validate environment consistency + print_status "2. Checking environment variable consistency..." + ./validate-env.sh + + print_status "3. Setting up Docker Buildx..." + + # Ensure buildx is available + if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available. Please update Docker to a version that supports Buildx." + exit 1 + fi + + # Create a new builder instance if it doesn't exist + if ! docker buildx ls | grep -q "rxminder-builder"; then + print_status "Creating new buildx builder instance..." + docker buildx create --name rxminder-builder --driver docker-container --bootstrap + fi + + # Use the builder + docker buildx use rxminder-builder + + print_status "4. Building multi-platform Docker image with buildx..." + + # Build the image with buildx for multiple platforms + docker buildx build --no-cache \ + --platform linux/amd64,linux/arm64 \ + --build-arg COUCHDB_USER="${COUCHDB_USER:-admin}" \ + --build-arg COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" \ + --build-arg VITE_COUCHDB_URL="${VITE_COUCHDB_URL:-http://localhost:5984}" \ + --build-arg VITE_COUCHDB_USER="${VITE_COUCHDB_USER:-admin}" \ + --build-arg VITE_COUCHDB_PASSWORD="${VITE_COUCHDB_PASSWORD:-change-this-secure-password}" \ + --build-arg APP_BASE_URL="${APP_BASE_URL:-http://localhost:8080}" \ + --build-arg VITE_GOOGLE_CLIENT_ID="${VITE_GOOGLE_CLIENT_ID:-}" \ + --build-arg VITE_GITHUB_CLIENT_ID="${VITE_GITHUB_CLIENT_ID:-}" \ + --build-arg MAILGUN_API_KEY="${MAILGUN_API_KEY:-}" \ + --build-arg MAILGUN_DOMAIN="${MAILGUN_DOMAIN:-}" \ + --build-arg MAILGUN_FROM_EMAIL="${MAILGUN_FROM_EMAIL:-}" \ + --build-arg NODE_ENV="${NODE_ENV:-production}" \ + -t rxminder-validation \ + --load \ + . + + print_success "Docker image built successfully" + + print_status "5. Testing container startup and health..." + + # Run container in background + docker run --rm -d \ + -p 8083:80 \ + --name rxminder-validation-test \ + rxminder-validation + + # Wait for container to start + sleep 5 + + # Check if container is running + if ! docker ps | grep -q rxminder-validation-test; then + print_error "Container failed to start" + docker logs rxminder-validation-test + exit 1 + fi + + print_success "Container started successfully" + + # Test health endpoint + print_status "5. Testing health endpoint..." + for i in {1..10}; do + if curl -s -f http://localhost:8083/health > /dev/null; then + print_success "Health endpoint responding" + break + elif [[ $i -eq 10 ]]; then + print_error "Health endpoint not responding after 10 attempts" + exit 1 + else + print_warning "Health endpoint not ready, retrying... ($i/10)" + sleep 2 + fi + done + + # Test main application + print_status "6. Testing main application..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8083) + if [[ $HTTP_CODE -eq 200 ]]; then + print_success "Main application responding (HTTP $HTTP_CODE)" + else + print_error "Main application not responding properly (HTTP $HTTP_CODE)" + exit 1 + fi + + # Test docker-compose build + print_status "7. Testing Docker Compose build..." + docker compose -f docker/docker-compose.yaml build frontend --no-cache + + print_success "Docker Compose build successful" + + # Test docker-compose with validation project name + print_status "8. Testing Docker Compose deployment..." + docker compose -f docker/docker-compose.yaml -p rxminder-validation up -d --build + + # Wait for services to start + sleep 10 + + # Check service health + if docker compose -f docker/docker-compose.yaml -p meds-validation ps | grep -q "Up"; then + print_success "Docker Compose services started successfully" + else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p meds-validation logs + exit 1 + fi + + # Test health of compose deployment + if curl -s -f http://localhost:8080/health > /dev/null; then + print_success "Docker Compose health endpoint responding" + else + print_warning "Docker Compose health endpoint not responding (may need CouchDB)" + fi + + print_status "9. Checking image size..." + IMAGE_SIZE=$(docker image inspect rxminder-validation --format='{{.Size}}' | numfmt --to=iec) + print_success "Image size: $IMAGE_SIZE" + + print_status "10. Validating security configuration..." + + # Check if image runs as non-root + USER_INFO=$(docker run --rm rxminder-validation whoami) + if [[ "$USER_INFO" != "root" ]]; then + print_success "Container runs as non-root user: $USER_INFO" + else + print_warning "Container runs as root user (security consideration)" + fi + + # Check nginx configuration + if docker run --rm rxminder-validation nginx -t 2>/dev/null; then + print_success "Nginx configuration is valid" + else + print_error "Nginx configuration has issues" + exit 1 + fi + + print_status "11. Final validation complete!" + + echo + echo "🎉 Deployment validation successful!" + echo + echo "Summary:" + echo "✅ Environment files validated" + echo "✅ Docker image builds successfully" + echo "✅ Container starts and runs healthy" + echo "✅ Health endpoints respond correctly" + echo "✅ Docker Compose deployment works" + echo "✅ Security configuration validated" + echo "✅ Image size optimized ($IMAGE_SIZE)" + echo + echo "Your deployment is ready for production! 🚀" + echo + echo "Next steps:" + echo "1. Configure production environment variables in .env" + echo "2. Run './deploy.sh production' for production deployment" + echo "3. Set up monitoring and backups" + echo "4. Configure SSL/TLS certificates" + echo + \ No newline at end of file diff --git a/scripts/buildx-helper.sh b/scripts/buildx-helper.sh new file mode 100755 index 0000000..9437452 --- /dev/null +++ b/scripts/buildx-helper.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# 🧪 Deployment Validation Script +# Validates complete deployment with all environment variables and health checks + +set -e + +echo "🚀 Starting deployment validation..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Cleanup function +cleanup() { + print_status "Cleaning up test containers..." + docker stop meds-validation-test 2>/dev/null || true + docker rm meds-validation-test 2>/dev/null || true + docker compose -f docker/docker-compose.yaml -p meds-validation down 2>/dev/null || true +} + +# Set trap for cleanup +trap cleanup EXIT + +print_status "1. Validating environment files..." + +# Check if required environment files exist +if [[ ! -f .env ]]; then + print_error ".env file not found. Run 'cp .env.example .env' and configure it." + exit 1 +fi + +if [[ ! -f .env.example ]]; then + print_error ".env.example file not found." + exit 1 +fi + +print_success "Environment files exist" + +# Validate environment consistency +print_status "2. Checking environment variable consistency..." +./validate-env.sh + +print_status "3. Setting up Docker Buildx..." + +# Ensure buildx is available +if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available. Please update Docker to a version that supports Buildx." + exit 1 +fi + +# Create a new builder instance if it doesn't exist +if ! docker buildx ls | grep -q "meds-builder"; then + print_status "Creating new buildx builder instance..." + docker buildx create --name meds-builder --driver docker-container --bootstrap +fi + +# Use the builder +docker buildx use meds-builder + +print_status "4. Building multi-platform Docker image with buildx..." + +# Build the image with buildx for multiple platforms +docker buildx build --no-cache \ +--platform linux/amd64,linux/arm64 \ +--build-arg COUCHDB_USER="${COUCHDB_USER:-admin}" \ +--build-arg COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg VITE_COUCHDB_URL="${VITE_COUCHDB_URL:-http://localhost:5984}" \ +--build-arg VITE_COUCHDB_USER="${VITE_COUCHDB_USER:-admin}" \ +--build-arg VITE_COUCHDB_PASSWORD="${VITE_COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg APP_BASE_URL="${APP_BASE_URL:-http://localhost:8080}" \ +--build-arg VITE_GOOGLE_CLIENT_ID="${VITE_GOOGLE_CLIENT_ID:-}" \ +--build-arg VITE_GITHUB_CLIENT_ID="${VITE_GITHUB_CLIENT_ID:-}" \ +--build-arg MAILGUN_API_KEY="${MAILGUN_API_KEY:-}" \ +--build-arg MAILGUN_DOMAIN="${MAILGUN_DOMAIN:-}" \ +--build-arg MAILGUN_FROM_EMAIL="${MAILGUN_FROM_EMAIL:-}" \ +--build-arg NODE_ENV="${NODE_ENV:-production}" \ +-t meds-validation \ +--load \ +. + +print_success "Docker image built successfully" + +print_status "5. Testing container startup and health..." + +# Run container in background +docker run --rm -d \ +-p 8083:80 \ +--name meds-validation-test \ +meds-validation + +# Wait for container to start +sleep 5 + +# Check if container is running +if ! docker ps | grep -q meds-validation-test; then + print_error "Container failed to start" + docker logs meds-validation-test + exit 1 +fi + +print_success "Container started successfully" + +# Test health endpoint +print_status "5. Testing health endpoint..." +for i in {1..10}; do + if curl -s -f http://localhost:8083/health > /dev/null; then + print_success "Health endpoint responding" + break + elif [[ $i -eq 10 ]]; then + print_error "Health endpoint not responding after 10 attempts" + exit 1 + else + print_warning "Health endpoint not ready, retrying... ($i/10)" + sleep 2 + fi +done + +# Test main application +print_status "6. Testing main application..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8083) +if [[ $HTTP_CODE -eq 200 ]]; then + print_success "Main application responding (HTTP $HTTP_CODE)" +else + print_error "Main application not responding properly (HTTP $HTTP_CODE)" + exit 1 +fi + +# Test docker-compose build +print_status "7. Testing Docker Compose build..." +docker compose -f docker/docker-compose.yaml build frontend --no-cache + +print_success "Docker Compose build successful" + +# Test docker-compose with validation project name +print_status "8. Testing Docker Compose deployment..." +docker compose -f docker/docker-compose.yaml -p meds-validation up -d --build + +# Wait for services to start +sleep 10 + +# Check service health +if docker compose -f docker/docker-compose.yaml -p meds-validation ps | grep -q "Up"; then + print_success "Docker Compose services started successfully" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p meds-validation logs + exit 1 +fi + +# Test health of compose deployment +if curl -s -f http://localhost:8080/health > /dev/null; then + print_success "Docker Compose health endpoint responding" +else + print_warning "Docker Compose health endpoint not responding (may need CouchDB)" +fi + +print_status "9. Checking image size..." +IMAGE_SIZE=$(docker image inspect meds-validation --format='{{.Size}}' | numfmt --to=iec) +print_success "Image size: $IMAGE_SIZE" + +print_status "10. Validating security configuration..." + +# Check if image runs as non-root +USER_INFO=$(docker run --rm meds-validation whoami) +if [[ "$USER_INFO" != "root" ]]; then + print_success "Container runs as non-root user: $USER_INFO" +else + print_warning "Container runs as root user (security consideration)" +fi + +# Check nginx configuration +if docker run --rm meds-validation nginx -t 2>/dev/null; then + print_success "Nginx configuration is valid" +else + print_error "Nginx configuration has issues" + exit 1 +fi + +print_status "11. Final validation complete!" + +echo +echo "🎉 Deployment validation successful!" +echo +echo "Summary:" +echo "✅ Environment files validated" +echo "✅ Docker image builds successfully" +echo "✅ Container starts and runs healthy" +echo "✅ Health endpoints respond correctly" +echo "✅ Docker Compose deployment works" +echo "✅ Security configuration validated" +echo "✅ Image size optimized ($IMAGE_SIZE)" +echo +echo "Your deployment is ready for production! 🚀" +echo +echo "Next steps:" +echo "1. Configure production environment variables in .env" +echo "2. Run './deploy.sh production' for production deployment" +echo "3. Set up monitoring and backups" +echo "4. Configure SSL/TLS certificates" +echo diff --git a/scripts/deploy-k8s.sh b/scripts/deploy-k8s.sh new file mode 100755 index 0000000..b52a7ba --- /dev/null +++ b/scripts/deploy-k8s.sh @@ -0,0 +1,274 @@ +#!/bin/bash + +# Kubernetes deployment script with environment variable substitution +# This script processes template files and applies them to Kubernetes + +set -euo pipefail + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +K8S_DIR="$SCRIPT_DIR/k8s" +TEMP_DIR="/tmp/meds-k8s-deploy" + +# Function to print colored output +print_info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +# Function to load environment variables +load_env() { + local env_file="$1" + if [[ -f "$env_file" ]]; then + print_info "Loading environment from $env_file" + # Export variables from .env file + set -a + source "$env_file" + set +a + else + print_warning "Environment file $env_file not found" + fi +} + +# Function to substitute environment variables in template files +substitute_templates() { + print_info "Processing template files..." + + # Create temporary directory + mkdir -p "$TEMP_DIR" + + # Process each template file + for template_file in "$K8S_DIR"/*.template; do + if [[ -f "$template_file" ]]; then + local filename=$(basename "$template_file" .template) + local output_file="$TEMP_DIR/$filename" + + print_info "Processing template: $filename" + + # Substitute environment variables + envsubst < "$template_file" > "$output_file" + + print_success "Generated: $output_file" + fi + done +} + +# Function to validate required environment variables +validate_env() { + local required_vars=("INGRESS_HOST") + local missing_vars=() + + for var in "${required_vars[@]}"; do + if [[ -z "${!var:-}" ]]; then + missing_vars+=("$var") + fi + done + + if [[ ${#missing_vars[@]} -gt 0 ]]; then + print_error "Missing required environment variables:" + for var in "${missing_vars[@]}"; do + echo " - $var" + done + echo "" + echo "Please set these variables in your .env file or environment." + exit 1 + fi +} + +# Function to apply Kubernetes manifests +apply_manifests() { + local manifest_dir="$1" + + print_info "Applying Kubernetes manifests from $manifest_dir" + + # Apply non-template files first + for manifest_file in "$K8S_DIR"/*.yaml; do + if [[ -f "$manifest_file" && ! "$manifest_file" =~ \.template$ ]]; then + print_info "Applying: $(basename "$manifest_file")" + kubectl apply -f "$manifest_file" + fi + done + + # Apply processed template files + if [[ -d "$TEMP_DIR" ]]; then + for manifest_file in "$TEMP_DIR"/*.yaml; do + if [[ -f "$manifest_file" ]]; then + print_info "Applying: $(basename "$manifest_file")" + kubectl apply -f "$manifest_file" + fi + done + fi +} + +# Function to cleanup temporary files +cleanup() { + if [[ -d "$TEMP_DIR" ]]; then + print_info "Cleaning up temporary files..." + rm -rf "$TEMP_DIR" + fi +} + +# Function to show deployment status +show_status() { + print_info "Deployment Status:" + echo "" + + print_info "Pods:" + kubectl get pods -l app=rxminder + echo "" + + print_info "Services:" + kubectl get services -l app=rxminder + echo "" + + print_info "Ingress:" + kubectl get ingress -l app=rxminder + echo "" + + if [[ -n "${INGRESS_HOST:-}" ]]; then + print_success "Application should be available at: http://$INGRESS_HOST" + fi +} + +# Function to show usage +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -e, --env FILE Environment file to load (default: .env)" + echo " -d, --dry-run Show what would be applied without applying" + echo " -s, --status Show deployment status only" + echo " -c, --cleanup Cleanup temporary files and exit" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 Deploy with default .env file" + echo " $0 -e .env.prod Deploy with production environment" + echo " $0 --dry-run Preview what would be deployed" + echo " $0 --status Check deployment status" +} + +# Main function +main() { + local env_file=".env" + local dry_run=false + local status_only=false + local cleanup_only=false + + # Parse command line arguments + while [[ $# -gt 0 ]]; do + case $1 in + -e|--env) + env_file="$2" + shift 2 + ;; + -d|--dry-run) + dry_run=true + shift + ;; + -s|--status) + status_only=true + shift + ;; + -c|--cleanup) + cleanup_only=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac + done + + # Handle cleanup only + if [[ "$cleanup_only" == true ]]; then + cleanup + exit 0 + fi + + # Handle status only + if [[ "$status_only" == true ]]; then + show_status + exit 0 + fi + + # Check if kubectl is available + if ! command -v kubectl &> /dev/null; then + print_error "kubectl is not installed or not in PATH" + exit 1 + fi + + # Check if we can connect to Kubernetes cluster + if ! kubectl cluster-info &> /dev/null; then + print_error "Cannot connect to Kubernetes cluster" + print_info "Make sure your kubectl is configured correctly" + exit 1 + fi + + print_info "🚀 Deploying Medication Reminder App to Kubernetes" + echo "" + + # Load environment variables + load_env "$env_file" + + # Validate required environment variables + validate_env + + # Process templates + substitute_templates + + if [[ "$dry_run" == true ]]; then + print_info "Dry run mode - showing generated manifests:" + echo "" + + for manifest_file in "$TEMP_DIR"/*.yaml; do + if [[ -f "$manifest_file" ]]; then + echo "=== $(basename "$manifest_file") ===" + cat "$manifest_file" + echo "" + fi + done + else + # Apply manifests + apply_manifests "$K8S_DIR" + + print_success "Deployment completed!" + echo "" + + # Show status + show_status + fi + + # Cleanup + cleanup +} + +# Trap to ensure cleanup happens +trap cleanup EXIT + +# Run main function +main "$@" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..9437452 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# 🧪 Deployment Validation Script +# Validates complete deployment with all environment variables and health checks + +set -e + +echo "🚀 Starting deployment validation..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Cleanup function +cleanup() { + print_status "Cleaning up test containers..." + docker stop meds-validation-test 2>/dev/null || true + docker rm meds-validation-test 2>/dev/null || true + docker compose -f docker/docker-compose.yaml -p meds-validation down 2>/dev/null || true +} + +# Set trap for cleanup +trap cleanup EXIT + +print_status "1. Validating environment files..." + +# Check if required environment files exist +if [[ ! -f .env ]]; then + print_error ".env file not found. Run 'cp .env.example .env' and configure it." + exit 1 +fi + +if [[ ! -f .env.example ]]; then + print_error ".env.example file not found." + exit 1 +fi + +print_success "Environment files exist" + +# Validate environment consistency +print_status "2. Checking environment variable consistency..." +./validate-env.sh + +print_status "3. Setting up Docker Buildx..." + +# Ensure buildx is available +if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available. Please update Docker to a version that supports Buildx." + exit 1 +fi + +# Create a new builder instance if it doesn't exist +if ! docker buildx ls | grep -q "meds-builder"; then + print_status "Creating new buildx builder instance..." + docker buildx create --name meds-builder --driver docker-container --bootstrap +fi + +# Use the builder +docker buildx use meds-builder + +print_status "4. Building multi-platform Docker image with buildx..." + +# Build the image with buildx for multiple platforms +docker buildx build --no-cache \ +--platform linux/amd64,linux/arm64 \ +--build-arg COUCHDB_USER="${COUCHDB_USER:-admin}" \ +--build-arg COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg VITE_COUCHDB_URL="${VITE_COUCHDB_URL:-http://localhost:5984}" \ +--build-arg VITE_COUCHDB_USER="${VITE_COUCHDB_USER:-admin}" \ +--build-arg VITE_COUCHDB_PASSWORD="${VITE_COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg APP_BASE_URL="${APP_BASE_URL:-http://localhost:8080}" \ +--build-arg VITE_GOOGLE_CLIENT_ID="${VITE_GOOGLE_CLIENT_ID:-}" \ +--build-arg VITE_GITHUB_CLIENT_ID="${VITE_GITHUB_CLIENT_ID:-}" \ +--build-arg MAILGUN_API_KEY="${MAILGUN_API_KEY:-}" \ +--build-arg MAILGUN_DOMAIN="${MAILGUN_DOMAIN:-}" \ +--build-arg MAILGUN_FROM_EMAIL="${MAILGUN_FROM_EMAIL:-}" \ +--build-arg NODE_ENV="${NODE_ENV:-production}" \ +-t meds-validation \ +--load \ +. + +print_success "Docker image built successfully" + +print_status "5. Testing container startup and health..." + +# Run container in background +docker run --rm -d \ +-p 8083:80 \ +--name meds-validation-test \ +meds-validation + +# Wait for container to start +sleep 5 + +# Check if container is running +if ! docker ps | grep -q meds-validation-test; then + print_error "Container failed to start" + docker logs meds-validation-test + exit 1 +fi + +print_success "Container started successfully" + +# Test health endpoint +print_status "5. Testing health endpoint..." +for i in {1..10}; do + if curl -s -f http://localhost:8083/health > /dev/null; then + print_success "Health endpoint responding" + break + elif [[ $i -eq 10 ]]; then + print_error "Health endpoint not responding after 10 attempts" + exit 1 + else + print_warning "Health endpoint not ready, retrying... ($i/10)" + sleep 2 + fi +done + +# Test main application +print_status "6. Testing main application..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8083) +if [[ $HTTP_CODE -eq 200 ]]; then + print_success "Main application responding (HTTP $HTTP_CODE)" +else + print_error "Main application not responding properly (HTTP $HTTP_CODE)" + exit 1 +fi + +# Test docker-compose build +print_status "7. Testing Docker Compose build..." +docker compose -f docker/docker-compose.yaml build frontend --no-cache + +print_success "Docker Compose build successful" + +# Test docker-compose with validation project name +print_status "8. Testing Docker Compose deployment..." +docker compose -f docker/docker-compose.yaml -p meds-validation up -d --build + +# Wait for services to start +sleep 10 + +# Check service health +if docker compose -f docker/docker-compose.yaml -p meds-validation ps | grep -q "Up"; then + print_success "Docker Compose services started successfully" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p meds-validation logs + exit 1 +fi + +# Test health of compose deployment +if curl -s -f http://localhost:8080/health > /dev/null; then + print_success "Docker Compose health endpoint responding" +else + print_warning "Docker Compose health endpoint not responding (may need CouchDB)" +fi + +print_status "9. Checking image size..." +IMAGE_SIZE=$(docker image inspect meds-validation --format='{{.Size}}' | numfmt --to=iec) +print_success "Image size: $IMAGE_SIZE" + +print_status "10. Validating security configuration..." + +# Check if image runs as non-root +USER_INFO=$(docker run --rm meds-validation whoami) +if [[ "$USER_INFO" != "root" ]]; then + print_success "Container runs as non-root user: $USER_INFO" +else + print_warning "Container runs as root user (security consideration)" +fi + +# Check nginx configuration +if docker run --rm meds-validation nginx -t 2>/dev/null; then + print_success "Nginx configuration is valid" +else + print_error "Nginx configuration has issues" + exit 1 +fi + +print_status "11. Final validation complete!" + +echo +echo "🎉 Deployment validation successful!" +echo +echo "Summary:" +echo "✅ Environment files validated" +echo "✅ Docker image builds successfully" +echo "✅ Container starts and runs healthy" +echo "✅ Health endpoints respond correctly" +echo "✅ Docker Compose deployment works" +echo "✅ Security configuration validated" +echo "✅ Image size optimized ($IMAGE_SIZE)" +echo +echo "Your deployment is ready for production! 🚀" +echo +echo "Next steps:" +echo "1. Configure production environment variables in .env" +echo "2. Run './deploy.sh production' for production deployment" +echo "3. Set up monitoring and backups" +echo "4. Configure SSL/TLS certificates" +echo diff --git a/scripts/gitea-deploy.sh b/scripts/gitea-deploy.sh new file mode 100755 index 0000000..2047881 --- /dev/null +++ b/scripts/gitea-deploy.sh @@ -0,0 +1,382 @@ +#!/bin/bash + +# 🧪 Deployment Validation Script +# Validates complete deployment with all environment variables and health checks + +set -e + +echo "🚀 Starting deploymif docker compose -f docker/docker-compose.yaml -p rxminder-validation ps | grep -q "Up"; then + print_success "Docker Compose setup completed successfully!" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p rxminder-validation logsalidation..." + + # Colors for output + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' # No Color + + # Function to print colored output + print_status() { + echo -e "${BLUE}[INFO]${NC} $1" + } + + print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" + } + + print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" + } + + print_error() { + echo -e "${RED}[ERROR]${NC} $1" + } + + # Cleanup function + cleanup() { + print_status "Cleaning up test containers..." + docker stop rxminder-validation-test 2>/dev/null || true + docker rm rxminder-validation-test 2>/dev/null || true + docker compose -f docker/docker-compose.yaml -p rxminder-validation down 2>/dev/null || true + } + + # Set trap for cleanup + trap cleanup EXIT + + print_status "1. Validating environment files..." + + # Check if required environment files exist + if [[ ! -f .env ]]; then + print_error ".env file not found. Run 'cp .env.example .env' and configure it." + exit 1 + fi + + if [[ ! -f .env.example ]]; then + print_error ".env.example file not found." + exit 1 + fi + + print_success "Environment files exist" + + # Validate environment consistency + print_status "2. Checking environment variable consistency..." + ./validate-env.sh + + print_status "3. Setting up Docker Buildx..." + + # Ensure buildx is available + if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available. Please update Docker to a version that supports Buildx." + exit 1 + fi + + # Create a new builder instance if it doesn't exist + if ! docker buildx ls | grep -q "rxminder-builder"; then + print_status "Creating new buildx builder instance..." + docker buildx create --name rxminder-builder --driver docker-container --bootstrap + fi + + # Use the builder + docker buildx use rxminder-builder + + print_status "4. Building multi-platform Docker image with buildx..." + + # Build the image with buildx for multiple platforms + docker buildx build --no-cache \ + --platform linux/amd64,linux/arm64 \ + --build-arg COUCHDB_USER="${COUCHDB_USER:-admin}" \ + --build-arg COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" \ + --build-arg VITE_COUCHDB_URL="${VITE_COUCHDB_URL:-http://localhost:5984}" \ + --build-arg VITE_COUCHDB_USER="${VITE_COUCHDB_USER:-admin}" \ + --build-arg VITE_COUCHDB_PASSWORD="${VITE_COUCHDB_PASSWORD:-change-this-secure-password}" \ + --build-arg APP_BASE_URL="${APP_BASE_URL:-http://localhost:8080}" \ + --build-arg VITE_GOOGLE_CLIENT_ID="${VITE_GOOGLE_CLIENT_ID:-}" \ + --build-arg VITE_GITHUB_CLIENT_ID="${VITE_GITHUB_CLIENT_ID:-}" \ + --build-arg MAILGUN_API_KEY="${MAILGUN_API_KEY:-}" \ + --build-arg MAILGUN_DOMAIN="${MAILGUN_DOMAIN:-}" \ + --build-arg MAILGUN_FROM_EMAIL="${MAILGUN_FROM_EMAIL:-}" \ + --build-arg NODE_ENV="${NODE_ENV:-production}" \ + -t rxminder-validation \ + --load \ + . + + print_success "Docker image built successfully" + + print_status "5. Testing container startup and health..." + + # Run container in background + docker run --rm -d \ + -p 8083:80 \ + --name rxminder-validation-test \ + rxminder-validation + + # Wait for container to start + sleep 5 + + # Check if container is running + if ! docker ps | grep -q rxminder-validation-test; then + print_error "Container failed to start" + docker logs rxminder-validation-test + exit 1 + fi + + print_success "Container started successfully" + + # Test health endpoint + print_status "5. Testing health endpoint..." + for i in {1..10}; do + if curl -s -f http://localhost:8083/health > /dev/null; then + print_success "Health endpoint responding" + break + elif [[ $i -eq 10 ]]; then + print_error "Health endpoint not responding after 10 attempts" + exit 1 + else + print_warning "Health endpoint not ready, retrying... ($i/10)" + sleep 2 + fi + done + + # Test main application + print_status "6. Testing main application..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8083) + if [[ $HTTP_CODE -eq 200 ]]; then + print_success "Main application responding (HTTP $HTTP_CODE)" + else + print_error "Main application not responding properly (HTTP $HTTP_CODE)" + exit 1 + fi + + # Test docker-compose build + print_status "7. Testing Docker Compose build..." + docker compose -f docker/docker-compose.yaml build frontend --no-cache + + print_success "Docker Compose build successful" + + # Test docker-compose with validation project name + print_status "8. Testing Docker Compose deployment..." + docker compose -f docker/docker-compose.yaml -p rxminder-validation up -d --build + + # Wait for services to start + sleep 10 + + # Check service health + if docker compose -f docker/docker-compose.yaml -p meds-validation ps | grep -q "Up"; then + print_success "Docker Compose services started successfully" + else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p meds-validation logs + exit 1 + fi + + # Test health of compose deployment + if curl -s -f http://localhost:8080/health > /dev/null; then + print_success "Docker Compose health endpoint responding" + else + print_warning "Docker Compose health endpoint not responding (may need CouchDB)" + fi + + print_status "9. Checking image size..." + IMAGE_SIZE=$(docker image inspect rxminder-validation --format='{{.Size}}' | numfmt --to=iec) + print_success "Image size: $IMAGE_SIZE" + + print_status "10. Validating security configuration..." + + # Check if image runs as non-root + USER_INFO=$(docker run --rm rxminder-validation whoami) + if [[ "$USER_INFO" != "root" ]]; then + print_success "Container runs as non-root user: $USER_INFO" + else + print_warning "Container runs as root user (security consideration)" + fi + + # Check nginx configuration + if docker run --rm rxminder-validation nginx -t 2>/dev/null; then + print_success "Nginx configuration is valid" + else + print_error "Nginx configuration has issues" + exit 1 + fi + + print_status "11. Final validation complete!" + + echo + echo "🎉 Deployment validation successful!" + echo + echo "Summary:" + echo "✅ Environment files validated" + echo "✅ Docker image builds successfully" + echo "✅ Container starts and runs healthy" + echo "✅ Health endpoints respond correctly" + echo "✅ Docker Compose deployment works" + echo "✅ Security configuration validated" + echo "✅ Image size optimized ($IMAGE_SIZE)" + echo + echo "Your deployment is ready for production! 🚀" + echo + echo "Next steps:" + echo "1. Configure production environment variables in .env" + echo "2. Run './deploy.sh production' for production deployment" + echo "3. Set up monitoring and backups" + echo "4. Configure SSL/TLS certificates" + echo + + # Load Gitea-specific environment variables + REGISTRY=${GITEA_SERVER_URL#https://} + IMAGE_NAME=${GITEA_REPOSITORY} + IMAGE_TAG=${GITEA_SHA:0:8} + + print_status "Registry: $REGISTRY" + print_status "Image: $IMAGE_NAME:$IMAGE_TAG" +fi + +# Check if .env file exists +if [ ! -f ".env" ]; then + print_warning ".env file not found, using defaults" + + # Create minimal .env for Gitea deployment + cat > .env << EOF +COUCHDB_USER=admin +COUCHDB_PASSWORD=change-this-secure-password +VITE_COUCHDB_URL=http://couchdb:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=change-this-secure-password +APP_BASE_URL=http://localhost:8080 +NODE_ENV=production +EOF + + print_warning "Created default .env file - please update with your credentials" +fi + +# Load environment variables +print_status "Loading environment variables from .env file..." +export $(cat .env | grep -v '^#' | xargs) + +# Validate required environment variables +REQUIRED_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "VITE_COUCHDB_URL") +for var in "${REQUIRED_VARS[@]}"; do + if [ -z "${!var}" ]; then + print_error "Required environment variable $var is not set" + exit 1 + fi +done + +print_success "Environment variables validated" + +# Function to deploy via Docker Compose +deploy_compose() { + print_status "Deploying with Docker Compose..." + + # Export image variables for compose + export IMAGE_TAG + export REGISTRY + export IMAGE_NAME + + # Use the built image from registry if available + if [ "$GITEA_ACTIONS" = "true" ]; then + # Override the image in docker-compose + export FRONTEND_IMAGE="$REGISTRY/$IMAGE_NAME:$IMAGE_TAG" + print_status "Using Gitea Actions built image: $FRONTEND_IMAGE" + fi + + # Pull the latest images + print_status "Pulling latest images..." + docker-compose -f docker/docker-compose.yaml pull || print_warning "Failed to pull some images" + + # Start services + print_status "Starting services..." + docker-compose -f docker/docker-compose.yaml up -d + + # Wait for services + print_status "Waiting for services to be ready..." + sleep 10 + + # Health check + print_status "Checking service health..." + if curl -f http://localhost:8080/health > /dev/null 2>&1; then + print_success "Frontend service is healthy" + else + print_warning "Frontend health check failed, checking logs..." + docker-compose -f docker/docker-compose.yaml logs frontend + fi + + if curl -f http://localhost:5984/_up > /dev/null 2>&1; then + print_success "CouchDB service is healthy" + else + print_warning "CouchDB health check failed" + fi +} + +# Function to deploy via Kubernetes +deploy_k8s() { + print_status "Deploying to Kubernetes..." + + if ! command -v kubectl &> /dev/null; then + print_error "kubectl is not installed" + exit 1 + fi + + # Update image in k8s manifests + if [ "$GITEA_ACTIONS" = "true" ]; then + print_status "Updating Kubernetes manifests with new image..." + sed -i "s|image:.*rxminder.*|image: $REGISTRY/$IMAGE_NAME:$IMAGE_TAG|g" k8s/frontend-deployment.yaml + fi + + # Apply manifests + print_status "Applying Kubernetes manifests..." + kubectl apply -f k8s/ + + # Wait for rollout + print_status "Waiting for deployment rollout..." + kubectl rollout status deployment/frontend-deployment + + print_success "Kubernetes deployment completed" +} + +# Main deployment logic +case "$ENVIRONMENT" in + "production"|"prod") + print_status "Deploying to production environment" + deploy_compose + ;; + "kubernetes"|"k8s") + print_status "Deploying to Kubernetes environment" + deploy_k8s + ;; + "staging") + print_status "Deploying to staging environment" + # Use staging-specific configurations + export APP_BASE_URL="http://staging.localhost:8080" + deploy_compose + ;; + *) + print_error "Unknown environment: $ENVIRONMENT" + echo "Available environments: production, kubernetes, staging" + exit 1 + ;; +esac + +print_success "Deployment to $ENVIRONMENT completed successfully! 🎉" + +# Post-deployment tasks +print_status "Running post-deployment tasks..." + +# Cleanup old images (optional) +if [ "$CLEANUP_OLD_IMAGES" = "true" ]; then + print_status "Cleaning up old Docker images..." + docker image prune -f --filter "until=72h" || print_warning "Image cleanup failed" +fi + +# Send notification (if configured) +if [ -n "$DEPLOYMENT_WEBHOOK_URL" ]; then + print_status "Sending deployment notification..." + curl -X POST "$DEPLOYMENT_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "{\"text\":\"✅ RxMinder deployed to $ENVIRONMENT\", \"environment\":\"$ENVIRONMENT\", \"image\":\"$REGISTRY/$IMAGE_NAME:$IMAGE_TAG\"}" \ + || print_warning "Failed to send notification" +fi + +print_success "All tasks completed! 🚀" diff --git a/scripts/gitea-helper.sh b/scripts/gitea-helper.sh new file mode 100755 index 0000000..898c275 --- /dev/null +++ b/scripts/gitea-helper.sh @@ -0,0 +1,518 @@ +#!/bin/bash + +# gitea-deploy.sh - Gitea-specific deployment script +# Usage: ./gitea-deploy.sh [environment] [image-tag] + +set -e # Exit on any error + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# Load environment variables from .env file if it exists +if [ -f "$PROJECT_DIR/.env" ]; then + export $(cat "$PROJECT_DIR/.env" | grep -v '^#' | grep -E '^[A-Z_]+=.*' | xargs) +fi + +ENVIRONMENT=${1:-production} +IMAGE_TAG=${2:-latest} +REGISTRY=${GITEA_REGISTRY:-${CONTAINER_REGISTRY:-"ghcr.io"}} +IMAGE_NAME=${GITEA_REPOSITORY:-${CONTAINER_REPOSITORY:-"rxminder"}} + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_status() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +echo "🚀 Deploying RxMinder from Gitea to $ENVIRONMENT environment..." + +# Check if running in Gitea Actions +if [ "$GITEA_ACTIONS" = "true" ]; then + print_status "Running in Gitea Actions environment" + + # Load Gitea-specific environment variables + REGISTRY=${GITEA_SERVER_URL#https://} + IMAGE_NAME=${GITEA_REPOSITORY} + IMAGE_TAG=${GITEA_SHA:0:8} + + print_status "Registry: $REGISTRY" + print_status "Image: $IMAGE_NAME:$IMAGE_TAG" +fi + +# Check if .env file exists +if [ ! -f ".env" ]; then + print_warning ".env file not found, using defaults" + + # Create minimal .env for Gitea deployment + cat > .env << EOF +COUCHDB_USER=admin +COUCHDB_PASSWORD=change-this-secure-password +VITE_COUCHDB_URL=http://couchdb:5984 +VITE_COUCHDB_USER=admin +VITE_COUCHDB_PASSWORD=change-this-secure-password +APP_BASE_URL=http://localhost:8080 +NODE_ENV=production +EOF + + print_warning "Created default .env file - please update with your credentials" +fi + +# Load environment variables +print_status "Loading environment variables from .env file..." +export $(cat .env | grep -v '^#' | xargs) + +# Validate required environment variables +REQUIRED_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "VITE_COUCHDB_URL") +for var in "${REQUIRED_VARS[@]}"; do + if [ -z "${!var}" ]; then + print_error "Required environment variable $var is not set" + exit 1 + fi +done + +print_success "Environment variables validated" + +# Function to deploy via Docker Compose +deploy_compose() { + print_status "Deploying with Docker Compose..." + + # Export image variables for compose + export IMAGE_TAG + export REGISTRY + export IMAGE_NAME + + # Use the built image from registry if available + if [ "$GITEA_ACTIONS" = "true" ]; then + # Override the image in docker-compose + export FRONTEND_IMAGE="$REGISTRY/$IMAGE_NAME:$IMAGE_TAG" + print_status "Using Gitea Actions built image: $FRONTEND_IMAGE" + fi + + # Pull the latest images + print_status "Pulling latest images..." + docker-compose -f docker/docker-compose.yaml pull || print_warning "Failed to pull some images" + + # Start services + print_status "Starting services..." + docker-compose -f docker/docker-compose.yaml up -d + + # Wait for services + print_status "Waiting for services to be ready..." + sleep 10 + + # Health check + print_status "Checking service health..." + if curl -f http://localhost:8080/health > /dev/null 2>&1; then + print_success "Frontend service is healthy" + else + print_warning "Frontend health check failed, checking logs..." + docker-compose -f docker/docker-compose.yaml logs frontend + fi + + if curl -f http://localhost:5984/_up > /dev/null 2>&1; then + print_success "CouchDB service is healthy" + else + print_warning "CouchDB health check failed" + fi +} + +# Function to deploy via Kubernetes +deploy_k8s() { + print_status "Deploying to Kubernetes..." + + if ! command -v kubectl &> /dev/null; then + print_error "kubectl is not installed" + exit 1 + fi + + # Update image in k8s manifests + if [ "$GITEA_ACTIONS" = "true" ]; then + print_status "Updating Kubernetes manifests with new image..." + sed -i "s|image:.*rxminder.*|image: $REGISTRY/$IMAGE_NAME:$IMAGE_TAG|g" k8s/frontend-deployment.yaml + fi + + # Apply manifests + print_status "Applying Kubernetes manifests..." + kubectl apply -f k8s/ + + # Wait for rollout + print_status "Waiting for deployment rollout..." + kubectl rollout status deployment/frontend-deployment + + print_success "Kubernetes deployment completed" +} + +# Main deployment logic +case "$ENVIRONMENT" in + "production"|"prod") + print_status "Deploying to production environment" + deploy_compose + ;; + "kubernetes"|"k8s") + print_status "Deploying to Kubernetes environment" + deploy_k8s + ;; + "staging") + print_status "Deploying to staging environment" + # Use staging-specific configurations + export APP_BASE_URL="http://staging.localhost:8080" + deploy_compose + ;; + *) + print_error "Unknown environment: $ENVIRONMENT" + echo "Available environments: production, kubernetes, staging" + exit 1 + ;; +esac + +print_success "Deployment to $ENVIRONMENT completed successfully! 🎉" + +# Post-deployment tasks +print_status "Running post-deployment tasks..." + +# Cleanup old images (optional) +if [ "$CLEANUP_OLD_IMAGES" = "true" ]; then + print_status "Cleaning up old Docker images..." + docker image prune -f --filter "until=72h" || print_warning "Image cleanup failed" +fi + +# Send notification (if configured) +if [ -n "$DEPLOYMENT_WEBHOOK_URL" ]; then + print_status "Sending deployment notification..." + curl -X POST "$DEPLOYMENT_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "{\"text\":\"✅ RxMinder deployed to $ENVIRONMENT\", \"environment\":\"$ENVIRONMENT\", \"image\":\"$REGISTRY/$IMAGE_NAME:$IMAGE_TAG\"}" \ + || print_warning "Failed to send notification" +fi + +print_success "All tasks completed! 🚀" + print_error "Docker is not installed" + exit 1 + fi + + # Check Docker Buildx + if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available" + exit 1 + fi + + # Check if in Gitea Actions environment + if [ "$GITEA_ACTIONS" = "true" ]; then + print_status "Running in Gitea Actions environment" + GITEA_REGISTRY=${GITEA_SERVER_URL#https://} + GITEA_REPOSITORY=${GITEA_REPOSITORY} + fi + + print_success "All requirements met" +} + +setup_buildx() { + print_status "Setting up Docker Buildx for Gitea..." + + # Create builder if it doesn't exist + if ! docker buildx ls | grep -q "gitea-builder"; then + print_status "Creating Gitea buildx builder..." + docker buildx create \ + --name gitea-builder \ + --driver docker-container \ + --bootstrap \ + --use + print_success "Gitea builder created" + else + docker buildx use gitea-builder + print_success "Using existing Gitea builder" + fi +} + +login_registry() { + print_status "Logging into Gitea registry..." + + if [ -z "$GITEA_TOKEN" ]; then + print_error "GITEA_TOKEN environment variable is required" + print_status "Set it with: export GITEA_TOKEN=your_token" + exit 1 + fi + + # Login to Gitea registry + echo "$GITEA_TOKEN" | docker login "$GITEA_REGISTRY" -u "$GITEA_ACTOR" --password-stdin + print_success "Logged into Gitea registry" +} + +build_local() { + print_status "Building for local development..." + + cd "$PROJECT_DIR" + + # Load environment variables + if [ -f ".env" ]; then + export $(cat .env | grep -v '^#' | xargs) + fi + + # Build with Gitea bake file + docker buildx bake \ + -f .gitea/gitea-bake.hcl \ + --set="*.platform=linux/amd64" \ + --load \ + dev + + print_success "Local build completed" +} + +build_multiplatform() { + local tag=${1:-$DEFAULT_TAG} + print_status "Building multi-platform image with tag: $tag..." + + cd "$PROJECT_DIR" + + # Load environment variables + if [ -f ".env" ]; then + export $(cat .env | grep -v '^#' | xargs) + fi + + # Export variables for bake + export TAG="$tag" + export GITEA_SHA=${GITEA_SHA:-$(git rev-parse --short HEAD 2>/dev/null || echo "dev")} + + # Build with Gitea bake file + docker buildx bake \ + -f .gitea/gitea-bake.hcl \ + app-ci + + print_success "Multi-platform build completed" +} + +build_staging() { + print_status "Building staging image..." + + cd "$PROJECT_DIR" + + # Load environment variables + if [ -f ".env.staging" ]; then + export $(cat .env.staging | grep -v '^#' | xargs) + elif [ -f ".env" ]; then + export $(cat .env | grep -v '^#' | xargs) + fi + + # Export variables for bake + export TAG="staging-$(date +%Y%m%d-%H%M%S)" + export GITEA_SHA=${GITEA_SHA:-$(git rev-parse --short HEAD 2>/dev/null || echo "staging")} + + # Build staging target + docker buildx bake \ + -f .gitea/gitea-bake.hcl \ + staging + + print_success "Staging build completed" +} + +build_production() { + local tag=${1:-$DEFAULT_TAG} + print_status "Building production image with tag: $tag..." + + cd "$PROJECT_DIR" + + # Load production environment variables + if [ -f ".env.production" ]; then + export $(cat .env.production | grep -v '^#' | xargs) + elif [ -f ".env" ]; then + export $(cat .env | grep -v '^#' | xargs) + fi + + # Export variables for bake + export TAG="$tag" + export GITEA_SHA=${GITEA_SHA:-$(git rev-parse --short HEAD 2>/dev/null || echo "prod")} + + # Build production target with full attestations + docker buildx bake \ + -f .gitea/gitea-bake.hcl \ + prod + + print_success "Production build completed" +} + +test_local() { + print_status "Running tests locally..." + + cd "$PROJECT_DIR" + + # Install dependencies if needed + if [ ! -d "node_modules" ]; then + print_status "Installing dependencies..." + bun install --frozen-lockfile + fi + + # Run linting + print_status "Running linter..." + bun run lint + + # Run type checking + print_status "Running type checker..." + bun run type-check + + # Run tests + print_status "Running tests..." + bun run test + + print_success "All tests passed" +} + +deploy() { + local environment=${1:-production} + local tag=${2:-latest} + + print_status "Deploying to $environment with tag $tag..." + + # Use the gitea-deploy script + "$SCRIPT_DIR/gitea-deploy.sh" "$environment" "$tag" +} + +cleanup() { + print_status "Cleaning up Gitea builder and images..." + + # Remove builder + if docker buildx ls | grep -q "gitea-builder"; then + docker buildx rm gitea-builder + print_success "Gitea builder removed" + fi + + # Clean up old images (keep last 3 tags) + print_status "Cleaning up old images..." + docker image prune -f --filter "until=72h" || print_warning "Image cleanup failed" + + print_success "Cleanup completed" +} + +show_status() { + print_status "Gitea CI/CD Status" + echo + + # Check environment + if [ "$GITEA_ACTIONS" = "true" ]; then + echo "🏃 Running in Gitea Actions" + echo "📦 Registry: $GITEA_REGISTRY" + echo "📋 Repository: $GITEA_REPOSITORY" + echo "🏷️ SHA: ${GITEA_SHA:-unknown}" + echo "🌿 Branch: ${GITEA_REF_NAME:-unknown}" + else + echo "💻 Running locally" + echo "📦 Registry: $GITEA_REGISTRY" + echo "📋 Repository: $GITEA_REPOSITORY" + fi + + echo + + # Check Docker and buildx + echo "🐳 Docker version: $(docker --version)" + echo "🔧 Buildx version: $(docker buildx version)" + + # Check builders + echo + echo "🏗️ Available builders:" + docker buildx ls +} + +show_help() { + cat << EOF +Gitea CI/CD Helper for RxMinder + +Usage: $0 [command] [options] + +Commands: + setup - Setup buildx builder for Gitea + login - Login to Gitea registry + build-local - Build for local development + build-multi [tag] - Build multi-platform image + build-staging - Build staging image + build-prod [tag] - Build production image + test - Run tests locally + deploy [env] [tag] - Deploy to environment + cleanup - Cleanup builders and images + status - Show CI/CD status + help - Show this help + +Examples: + $0 setup + $0 build-local + $0 build-multi v1.2.3 + $0 build-staging + $0 build-prod v1.2.3 + $0 test + $0 deploy production v1.2.3 + $0 deploy staging + $0 status + +Environment Variables: + GITEA_REGISTRY - Gitea registry URL (default: gitea.example.com) + GITEA_REPOSITORY - Repository name (default: user/rxminder) + GITEA_TOKEN - Gitea access token (required for registry) + GITEA_ACTOR - Gitea username (for registry login) + +EOF +} + +# Main command handling +case "${1:-help}" in + "setup") + check_requirements + setup_buildx + ;; + "login") + check_requirements + login_registry + ;; + "build-local") + check_requirements + setup_buildx + build_local + ;; + "build-multi") + check_requirements + setup_buildx + login_registry + build_multiplatform "$2" + ;; + "build-staging") + check_requirements + setup_buildx + login_registry + build_staging + ;; + "build-prod") + check_requirements + setup_buildx + login_registry + build_production "$2" + ;; + "test") + test_local + ;; + "deploy") + deploy "$2" "$3" + ;; + "cleanup") + cleanup + ;; + "status") + show_status + ;; + "help"|*) + show_help + ;; +esac diff --git a/scripts/k8s-deploy-template.sh b/scripts/k8s-deploy-template.sh new file mode 100755 index 0000000..d8c9a70 --- /dev/null +++ b/scripts/k8s-deploy-template.sh @@ -0,0 +1,330 @@ +#!/usr/bin/env bash + +# 🚀 RxMinder Template-based Kubernetes Deployment Script +# This script processes template files and applies them to Kubernetes + +set -euo pipefail + +# Script configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +K8S_DIR="$(dirname "$SCRIPT_DIR")/k8s" +ENV_FILE="$(dirname "$SCRIPT_DIR")/.env" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_status() { + echo -e "${BLUE}$1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +# Function to load environment variables +load_env() { + if [[ -f "$ENV_FILE" ]]; then + print_status "Loading environment variables from .env..." + # Export variables from .env file + set -a + source "$ENV_FILE" + set +a + print_success "Environment variables loaded" + else + print_warning ".env file not found at $ENV_FILE" + print_warning "Using default values. Copy .env.example to .env and customize." + fi +} + +# Function to substitute environment variables in templates +substitute_template() { + local template_file="$1" + local output_file="$2" + + print_status "Processing template: $template_file" + + # Use envsubst to substitute environment variables + if command -v envsubst >/dev/null 2>&1; then + envsubst < "$template_file" > "$output_file" + else + print_error "envsubst not found. Please install gettext package." + exit 1 + fi + + print_success "Generated: $output_file" +} + +# Function to apply Kubernetes resources +apply_k8s_resource() { + local resource_file="$1" + + if [[ -f "$resource_file" ]]; then + print_status "Applying Kubernetes resource: $resource_file" + if kubectl apply -f "$resource_file"; then + print_success "Applied: $resource_file" + else + print_error "Failed to apply: $resource_file" + return 1 + fi + else + print_warning "Resource file not found: $resource_file" + fi +} + +# Function to validate required environment variables +validate_env() { + local required_vars=( + "APP_NAME" + "DOCKER_IMAGE" + "COUCHDB_USER" + "COUCHDB_PASSWORD" + "INGRESS_HOST" + "STORAGE_CLASS" + "STORAGE_SIZE" + ) + + local missing_vars=() + + for var in "${required_vars[@]}"; do + if [[ -z "${!var:-}" ]]; then + missing_vars+=("$var") + fi + done + + if [[ ${#missing_vars[@]} -gt 0 ]]; then + print_error "Missing required environment variables:" + for var in "${missing_vars[@]}"; do + echo -e " ${RED}- $var${NC}" + done + print_warning "Please update your .env file with these variables." + exit 1 + fi + + print_success "All required environment variables are set" +} + +# Function to process all templates +process_templates() { + local temp_dir="/tmp/rxminder-k8s-$$" + mkdir -p "$temp_dir" + + print_status "Processing Kubernetes templates..." + + # Find all template files + local template_files=( + "$K8S_DIR/couchdb-secret.yaml.template" + "$K8S_DIR/ingress.yaml.template" + ) + + # Add any additional template files + for template_file in "$K8S_DIR"/*.template; do + if [[ -f "$template_file" ]]; then + template_files+=("$template_file") + fi + done + + # Process each template + for template_file in "${template_files[@]}"; do + if [[ -f "$template_file" ]]; then + local base_name + base_name="$(basename "$template_file" .template)" + local output_file="$temp_dir/$base_name" + substitute_template "$template_file" "$output_file" + fi + done + + echo "$temp_dir" +} + +# Function to deploy resources in correct order +deploy_resources() { + local resource_dir="$1" + + print_status "Deploying Kubernetes resources..." + + # Deploy in specific order for dependencies + local deployment_order=( + "couchdb-secret.yaml" + "couchdb-pvc.yaml" + "couchdb-service.yaml" + "couchdb-statefulset.yaml" + "configmap.yaml" + "frontend-deployment.yaml" + "frontend-service.yaml" + "ingress.yaml" + "$K8S_DIR/network-policy.yaml" + "$K8S_DIR/hpa.yaml" + ) + + for resource in "${deployment_order[@]}"; do + if [[ "$resource" == *.yaml ]]; then + # Check if it's a template-generated file + if [[ -f "$resource_dir/$(basename "$resource")" ]]; then + apply_k8s_resource "$resource_dir/$(basename "$resource")" + else + # Apply directly from k8s directory + apply_k8s_resource "$resource" + fi + fi + done +} + +# Function to run database seeding job +run_db_seed() { + print_status "Running database seed job..." + + # Apply the db-seed-job (which uses environment variables from secret) + if kubectl apply -f "$K8S_DIR/db-seed-job.yaml"; then + print_success "Database seed job submitted" + + # Wait for job completion + print_status "Waiting for database seed job to complete..." + if kubectl wait --for=condition=complete --timeout=300s job/db-seed-job; then + print_success "Database seeding completed successfully" + else + print_warning "Database seed job may have failed. Check logs:" + echo "kubectl logs job/db-seed-job" + fi + else + print_error "Failed to apply database seed job" + return 1 + fi +} + +# Function to display deployment status +show_status() { + print_status "Deployment Status:" + echo + + print_status "Pods:" + kubectl get pods -l app="${APP_NAME:-rxminder}" + echo + + print_status "Services:" + kubectl get services -l app="${APP_NAME:-rxminder}" + echo + + print_status "Ingress:" + kubectl get ingress + echo + + if [[ -n "${INGRESS_HOST:-}" ]]; then + print_success "Application should be available at: http://${INGRESS_HOST}" + fi +} + +# Function to cleanup temporary files +cleanup() { + if [[ -n "${temp_dir:-}" && -d "$temp_dir" ]]; then + rm -rf "$temp_dir" + fi +} + +# Main deployment function +main() { + local command="${1:-deploy}" + + case "$command" in + "deploy"|"apply") + print_status "🚀 Starting RxMinder Kubernetes deployment..." + echo + + # Set default values for required variables + export APP_NAME="${APP_NAME:-rxminder}" + export DOCKER_IMAGE="${DOCKER_IMAGE:-gitea-http.taildb3494.ts.net/will/meds:latest}" + export COUCHDB_USER="${COUCHDB_USER:-admin}" + export COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" + export INGRESS_HOST="${INGRESS_HOST:-rxminder.local}" + export STORAGE_CLASS="${STORAGE_CLASS:-longhorn}" + export STORAGE_SIZE="${STORAGE_SIZE:-5Gi}" + + load_env + validate_env + + # Process templates + temp_dir=$(process_templates) + trap cleanup EXIT + + # Deploy resources + deploy_resources "$temp_dir" + + # Run database seeding + run_db_seed + + # Show status + echo + show_status + + print_success "🎉 RxMinder deployment completed!" + ;; + + "status") + load_env + show_status + ;; + + "delete"|"cleanup") + print_status "🗑️ Cleaning up RxMinder deployment..." + kubectl delete all,pvc,secret,configmap,ingress -l app="${APP_NAME:-rxminder}" || true + kubectl delete job db-seed-job || true + print_success "Cleanup completed" + ;; + + "help"|"-h"|"--help") + echo "RxMinder Kubernetes Deployment Script" + echo + echo "Usage: $0 [command]" + echo + echo "Commands:" + echo " deploy Deploy RxMinder to Kubernetes (default)" + echo " status Show deployment status" + echo " delete Delete all RxMinder resources" + echo " help Show this help message" + echo + echo "Environment variables (set in .env):" + echo " APP_NAME Application name (default: rxminder)" + echo " DOCKER_IMAGE Container image to deploy" + echo " COUCHDB_USER Database username (default: admin)" + echo " COUCHDB_PASSWORD Database password (required)" + echo " INGRESS_HOST Ingress hostname (required)" + echo " STORAGE_CLASS Storage class for PVCs (default: longhorn)" + echo " STORAGE_SIZE Storage size for database (default: 5Gi)" + ;; + + *) + print_error "Unknown command: $command" + echo "Use '$0 help' for usage information" + exit 1 + ;; + esac +} + +# Check if kubectl is available +if ! command -v kubectl >/dev/null 2>&1; then + print_error "kubectl not found. Please install kubectl and configure it to connect to your cluster." + exit 1 +fi + +# Check if envsubst is available +if ! command -v envsubst >/dev/null 2>&1; then + print_error "envsubst not found. Please install the gettext package:" + echo " Ubuntu/Debian: sudo apt-get install gettext" + echo " macOS: brew install gettext" + echo " RHEL/CentOS: sudo yum install gettext" + exit 1 +fi + +# Run main function +main "$@" diff --git a/scripts/seed-production.js b/scripts/seed-production.js new file mode 100644 index 0000000..cfe894a --- /dev/null +++ b/scripts/seed-production.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node + +// Production database seeder script +// This script seeds the production CouchDB with default admin user + +import { readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectDir = resolve(__dirname, '..'); + +console.log('🌱 Starting production database seeding...'); + +// Load environment variables from .env file if it exists +try { + const envFile = resolve(projectDir, '.env'); + const envContent = readFileSync(envFile, 'utf8'); + + console.log('📄 Loading environment variables from .env file...'); + + envContent.split('\n').forEach(line => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) { + const [key, ...valueParts] = trimmed.split('='); + const value = valueParts.join('='); + if (!process.env[key]) { + process.env[key] = value; + } + } + }); +} catch (error) { + console.log( + 'ℹ️ No .env file found, using environment variables or defaults' + ); +} + +// Use environment variables with fallbacks +const COUCHDB_URL = + process.env.VITE_COUCHDB_URL || + process.env.COUCHDB_URL || + 'http://localhost:5984'; +const COUCHDB_USER = + process.env.VITE_COUCHDB_USER || process.env.COUCHDB_USER || 'admin'; +const COUCHDB_PASSWORD = + process.env.VITE_COUCHDB_PASSWORD || + process.env.COUCHDB_PASSWORD || + 'change-this-secure-password'; + +// Set environment variables for the seeder to use +process.env.VITE_COUCHDB_URL = COUCHDB_URL; +process.env.VITE_COUCHDB_USER = COUCHDB_USER; +process.env.VITE_COUCHDB_PASSWORD = COUCHDB_PASSWORD; + +console.log('🔗 CouchDB Configuration:'); +console.log(` URL: ${COUCHDB_URL}`); +console.log(` User: ${COUCHDB_USER}`); +console.log(` Password: ${'*'.repeat(COUCHDB_PASSWORD.length)}`); + +// Validate required environment variables +if (!COUCHDB_URL || !COUCHDB_USER || !COUCHDB_PASSWORD) { + console.error('❌ Missing required environment variables:'); + console.error(' VITE_COUCHDB_URL or COUCHDB_URL'); + console.error(' VITE_COUCHDB_USER or COUCHDB_USER'); + console.error(' VITE_COUCHDB_PASSWORD or COUCHDB_PASSWORD'); + console.error(''); + console.error('💡 Set these in your .env file or as environment variables'); + process.exit(1); +} + +async function seedDatabase() { + try { + // Import the seeder (this will use the production CouchDB due to env vars) + const { DatabaseSeeder } = await import('../services/database.seeder.ts'); + + // Wait a bit for databases to be initialized + console.log('⏳ Waiting for databases to initialize...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + + const seeder = new DatabaseSeeder(); + + console.log('📊 Seeding admin user...'); + await seeder.seedDefaultAdmin(); + + console.log('🎉 Production database seeding completed successfully!'); + console.log('🔐 You can now login with:'); + console.log(' Email: admin@localhost'); + console.log(' Password: change-this-secure-password'); + + process.exit(0); + } catch (error) { + console.error('❌ Seeding failed:', error); + process.exit(1); + } +} + +seedDatabase(); diff --git a/scripts/setup-e2e.sh b/scripts/setup-e2e.sh new file mode 100755 index 0000000..e099011 --- /dev/null +++ b/scripts/setup-e2e.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# 🎭 Playwright E2E Test Setup Script + +echo "🎭 Setting up Playwright for E2E testing..." + +# Check if we're in the right directory +if [[ ! -f "package.json" ]]; then + echo "❌ Error: Must be run from project root directory" + exit 1 +fi + +# Install Playwright if not already installed +echo "📦 Installing Playwright..." +if command -v bun &> /dev/null; then + bun add -D @playwright/test +else + npm install -D @playwright/test +fi + +# Install browser binaries +echo "🌐 Installing browser binaries..." +npx playwright install + +# Install system dependencies (Linux) +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "🐧 Installing system dependencies for Linux..." + npx playwright install-deps +fi + +# Create .gitignore entries for Playwright +echo "📝 Updating .gitignore for Playwright artifacts..." +if ! grep -q "test-results" .gitignore; then + echo "" >> .gitignore + echo "# Playwright artifacts" >> .gitignore + echo "test-results/" >> .gitignore + echo "playwright-report/" >> .gitignore + echo "playwright/.cache/" >> .gitignore +fi + +# Verify installation +echo "✅ Verifying Playwright installation..." +npx playwright --version + +echo "" +echo "🎉 Playwright setup complete!" +echo "" +echo "📋 Quick start commands:" +echo " bun run test:e2e # Run all E2E tests" +echo " bun run test:e2e:ui # Run tests in UI mode" +echo " bun run test:e2e:debug # Debug tests" +echo " bun run test:e2e:report # View test report" +echo "" +echo "📚 Documentation: tests/e2e/README.md" +echo "⚙️ Configuration: playwright.config.ts" diff --git a/scripts/setup-pre-commit.sh b/scripts/setup-pre-commit.sh new file mode 100755 index 0000000..8945e2d --- /dev/null +++ b/scripts/setup-pre-commit.sh @@ -0,0 +1,133 @@ +# Run lint-staged for file-specific checks +bunx lint-staged + +# Run type checking (doesn't need file filtering) +bun run type-check + +# Check for large files (similar to pre-commit check-added-large-files) +git diff --cached --name-only | while IFS= read -r file; do + if [ -f "$file" ]; then + size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo 0) + if [ "$size" -gt 500000 ]; then # 500KB limit + echo "Error: Large file detected: $file ($(echo $size | awk '{print int($1/1024)}')KB)" + echo "Consider using Git LFS for large files." + exit 1 + fi + fi +done + +# Check for merge conflict markers +if git diff --cached | grep -E "^[<>=]{7}" >/dev/null; then + echo "Error: Merge conflict markers detected" + exit 1 +fi + +# Check for private keys (basic check) +if git diff --cached --name-only | xargs grep -l "BEGIN.*PRIVATE KEY" 2>/dev/null; then + echo "Error: Private key detected in staged files" + exit 1 +fi + +echo "✅ Pre-commit checks passed!" +NC='\033[0m' # No Color + +echo -e "${GREEN}Setting up NodeJS-native pre-commit hooks and code formatters...${NC}" + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo -e "${RED}Error: Not a git repository. Please run this script from the project root.${NC}" + exit 1 +fi + +# Install dependencies +echo -e "${YELLOW}Installing dependencies...${NC}" +if command -v bun &> /dev/null; then + bun install +elif command -v npm &> /dev/null; then + npm install +else + echo -e "${RED}Error: Neither bun nor npm found. Please install one of them first.${NC}" + exit 1 +fi + +# Initialize Husky (NodeJS-native git hooks) +echo -e "${YELLOW}Setting up Husky git hooks...${NC}" +if command -v bun &> /dev/null; then + bunx husky init +elif command -v npm &> /dev/null; then + npx husky init +fi + +# Make pre-commit hook executable +chmod +x .husky/pre-commit + +# Run initial formatting +echo -e "${YELLOW}Running initial code formatting...${NC}" +if command -v bun &> /dev/null; then + bun run format +elif command -v npm &> /dev/null; then + npm run format +fi + +# Run initial linting +echo -e "${YELLOW}Running initial linting...${NC}" +if command -v bun &> /dev/null; then + bun run lint:fix +elif command -v npm &> /dev/null; then + npm run lint:fix +fi + +# Run initial markdown linting +echo -e "${YELLOW}Running initial markdown linting...${NC}" +if command -v bun &> /dev/null; then + bun run lint:markdown:fix || echo "Markdown linting completed with warnings" +elif command -v npm &> /dev/null; then + npm run lint:markdown:fix || echo "Markdown linting completed with warnings" +fi + +# Fix EditorConfig issues +echo -e "${YELLOW}Fixing EditorConfig issues...${NC}" +if command -v bun &> /dev/null; then + bun run fix:editorconfig || echo "EditorConfig fixes completed" +elif command -v npm &> /dev/null; then + npm run fix:editorconfig || echo "EditorConfig fixes completed" +fi + +echo -e "${GREEN}✅ NodeJS-native pre-commit hooks and code formatters have been set up successfully!${NC}" +echo "" +echo -e "${YELLOW}What was configured:${NC}" +echo " ✓ Husky git hooks (.husky/pre-commit)" +echo " ✓ Prettier code formatter (.prettierrc)" +echo " ✓ ESLint configuration (eslint.config.cjs)" +echo " ✓ EditorConfig (.editorconfig)" +echo " ✓ Markdownlint configuration (.markdownlint.json)" +echo " ✓ Secretlint configuration (.secretlintrc.json)" +echo " ✓ Lint-staged for efficient pre-commit formatting" +echo " ✓ Dockerfilelint for Docker file validation" +echo "" +echo -e "${YELLOW}Available commands:${NC}" +echo " • bun run format - Format all files with Prettier" +echo " • bun run lint - Run ESLint on TypeScript/JavaScript files" +echo " • bun run lint:fix - Run ESLint with auto-fix" +echo " • bun run lint:markdown - Check Markdown files" +echo " • bun run lint:markdown:fix - Fix Markdown files" +echo " • bun run lint:docker - Check Dockerfile" +echo " • bun run check:secrets - Check for secrets in files" +echo " • bun run check:editorconfig - Check EditorConfig compliance" +echo " • bun run fix:editorconfig - Fix EditorConfig issues" +echo " • bun run type-check - Run TypeScript type checking" +echo "" +echo -e "${YELLOW}What happens on commit:${NC}" +echo " 1. Lint-staged runs on changed files:" +echo " • ESLint auto-fix + Prettier formatting for JS/TS files" +echo " • Prettier formatting for JSON/YAML/MD/CSS files" +echo " • Markdownlint auto-fix for Markdown files" +echo " • Dockerfilelint for Dockerfile" +echo " • EditorConfig fixes for all files" +echo " 2. TypeScript type checking on entire project" +echo " 3. Large file detection (>500KB)" +echo " 4. Merge conflict marker detection" +echo " 5. Basic private key detection" +echo "" +echo -e "${GREEN}All commits will now be automatically checked and formatted! 🎉${NC}" +echo -e "${GREEN}This setup is 100% NodeJS-native - no Python dependencies required! 🚀${NC}" diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..9ea1d4c --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,225 @@ +#!/bin/bash + +# 🧪 Deployment Validation Script +# Validates complete deployment with all environment variables and health checks + +set -e + +echo "🚀 Starting deploymif docker compose -f docker/docker-compose.yaml -p rxminder-validation ps | grep -q "Up"; then + print_success "Docker Compose setup completed successfully!" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p rxminder-validation logsalidation..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Cleanup function +cleanup() { + print_status "Cleaning up test containers..." + docker stop rxminder-validation-test 2>/dev/null || true + docker rm rxminder-validation-test 2>/dev/null || true + docker compose -f docker/docker-compose.yaml -p rxminder-validation down 2>/dev/null || true +} + +# Set trap for cleanup +trap cleanup EXIT + +print_status "1. Validating environment files..." + +# Check if required environment files exist +if [[ ! -f .env ]]; then + print_error ".env file not found. Run 'cp .env.example .env' and configure it." + exit 1 +fi + +if [[ ! -f .env.example ]]; then + print_error ".env.example file not found." + exit 1 +fi + +print_success "Environment files exist" + +# Validate environment consistency +print_status "2. Checking environment variable consistency..." +./validate-env.sh + +print_status "3. Setting up Docker Buildx..." + +# Ensure buildx is available +if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available. Please update Docker to a version that supports Buildx." + exit 1 +fi + +# Create a new builder instance if it doesn't exist +if ! docker buildx ls | grep -q "rxminder-builder"; then + print_status "Creating new buildx builder instance..." + docker buildx create --name rxminder-builder --driver docker-container --bootstrap +fi + +# Use the builder +docker buildx use rxminder-builder + +print_status "4. Building multi-platform Docker image with buildx..." + +# Build the image with buildx for multiple platforms +docker buildx build --no-cache \ +--platform linux/amd64,linux/arm64 \ +--build-arg COUCHDB_USER="${COUCHDB_USER:-admin}" \ +--build-arg COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg VITE_COUCHDB_URL="${VITE_COUCHDB_URL:-http://localhost:5984}" \ +--build-arg VITE_COUCHDB_USER="${VITE_COUCHDB_USER:-admin}" \ +--build-arg VITE_COUCHDB_PASSWORD="${VITE_COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg APP_BASE_URL="${APP_BASE_URL:-http://localhost:8080}" \ +--build-arg VITE_GOOGLE_CLIENT_ID="${VITE_GOOGLE_CLIENT_ID:-}" \ +--build-arg VITE_GITHUB_CLIENT_ID="${VITE_GITHUB_CLIENT_ID:-}" \ +--build-arg MAILGUN_API_KEY="${MAILGUN_API_KEY:-}" \ +--build-arg MAILGUN_DOMAIN="${MAILGUN_DOMAIN:-}" \ +--build-arg MAILGUN_FROM_EMAIL="${MAILGUN_FROM_EMAIL:-}" \ +--build-arg NODE_ENV="${NODE_ENV:-production}" \ +-t rxminder-validation \ +--load \ +. + +print_success "Docker image built successfully" + +print_status "5. Testing container startup and health..." + +# Run container in background +docker run --rm -d \ +-p 8083:80 \ +--name rxminder-validation-test \ +rxminder-validation + +# Wait for container to start +sleep 5 + +# Check if container is running +if ! docker ps | grep -q rxminder-validation-test; then + print_error "Container failed to start" + docker logs rxminder-validation-test + exit 1 +fi + +print_success "Container started successfully" + +# Test health endpoint +print_status "5. Testing health endpoint..." +for i in {1..10}; do + if curl -s -f http://localhost:8083/health > /dev/null; then + print_success "Health endpoint responding" + break + elif [[ $i -eq 10 ]]; then + print_error "Health endpoint not responding after 10 attempts" + exit 1 + else + print_warning "Health endpoint not ready, retrying... ($i/10)" + sleep 2 + fi +done + +# Test main application +print_status "6. Testing main application..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8083) +if [[ $HTTP_CODE -eq 200 ]]; then + print_success "Main application responding (HTTP $HTTP_CODE)" +else + print_error "Main application not responding properly (HTTP $HTTP_CODE)" + exit 1 +fi + +# Test docker-compose build +print_status "7. Testing Docker Compose build..." +docker compose -f docker/docker-compose.yaml build frontend --no-cache + +print_success "Docker Compose build successful" + +# Test docker-compose with validation project name +print_status "8. Testing Docker Compose deployment..." +docker compose -f docker/docker-compose.yaml -p rxminder-validation up -d --build + +# Wait for services to start +sleep 10 + +# Check service health +if docker compose -f docker/docker-compose.yaml -p meds-validation ps | grep -q "Up"; then + print_success "Docker Compose services started successfully" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p meds-validation logs + exit 1 +fi + +# Test health of compose deployment +if curl -s -f http://localhost:8080/health > /dev/null; then + print_success "Docker Compose health endpoint responding" +else + print_warning "Docker Compose health endpoint not responding (may need CouchDB)" +fi + +print_status "9. Checking image size..." +IMAGE_SIZE=$(docker image inspect rxminder-validation --format='{{.Size}}' | numfmt --to=iec) +print_success "Image size: $IMAGE_SIZE" + +print_status "10. Validating security configuration..." + +# Check if image runs as non-root +USER_INFO=$(docker run --rm rxminder-validation whoami) +if [[ "$USER_INFO" != "root" ]]; then + print_success "Container runs as non-root user: $USER_INFO" +else + print_warning "Container runs as root user (security consideration)" +fi + +# Check nginx configuration +if docker run --rm rxminder-validation nginx -t 2>/dev/null; then + print_success "Nginx configuration is valid" +else + print_error "Nginx configuration has issues" + exit 1 +fi + +print_status "11. Final validation complete!" + +echo +echo "🎉 Deployment validation successful!" +echo +echo "Summary:" +echo "✅ Environment files validated" +echo "✅ Docker image builds successfully" +echo "✅ Container starts and runs healthy" +echo "✅ Health endpoints respond correctly" +echo "✅ Docker Compose deployment works" +echo "✅ Security configuration validated" +echo "✅ Image size optimized ($IMAGE_SIZE)" +echo +echo "Your deployment is ready for production! 🚀" +echo +echo "Next steps:" +echo "1. Configure production environment variables in .env" +echo "2. Run './deploy.sh production' for production deployment" +echo "3. Set up monitoring and backups" +echo "4. Configure SSL/TLS certificates" +echo diff --git a/scripts/validate-deployment.sh b/scripts/validate-deployment.sh new file mode 100755 index 0000000..9437452 --- /dev/null +++ b/scripts/validate-deployment.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# 🧪 Deployment Validation Script +# Validates complete deployment with all environment variables and health checks + +set -e + +echo "🚀 Starting deployment validation..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Cleanup function +cleanup() { + print_status "Cleaning up test containers..." + docker stop meds-validation-test 2>/dev/null || true + docker rm meds-validation-test 2>/dev/null || true + docker compose -f docker/docker-compose.yaml -p meds-validation down 2>/dev/null || true +} + +# Set trap for cleanup +trap cleanup EXIT + +print_status "1. Validating environment files..." + +# Check if required environment files exist +if [[ ! -f .env ]]; then + print_error ".env file not found. Run 'cp .env.example .env' and configure it." + exit 1 +fi + +if [[ ! -f .env.example ]]; then + print_error ".env.example file not found." + exit 1 +fi + +print_success "Environment files exist" + +# Validate environment consistency +print_status "2. Checking environment variable consistency..." +./validate-env.sh + +print_status "3. Setting up Docker Buildx..." + +# Ensure buildx is available +if ! docker buildx version >/dev/null 2>&1; then + print_error "Docker Buildx is not available. Please update Docker to a version that supports Buildx." + exit 1 +fi + +# Create a new builder instance if it doesn't exist +if ! docker buildx ls | grep -q "meds-builder"; then + print_status "Creating new buildx builder instance..." + docker buildx create --name meds-builder --driver docker-container --bootstrap +fi + +# Use the builder +docker buildx use meds-builder + +print_status "4. Building multi-platform Docker image with buildx..." + +# Build the image with buildx for multiple platforms +docker buildx build --no-cache \ +--platform linux/amd64,linux/arm64 \ +--build-arg COUCHDB_USER="${COUCHDB_USER:-admin}" \ +--build-arg COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg VITE_COUCHDB_URL="${VITE_COUCHDB_URL:-http://localhost:5984}" \ +--build-arg VITE_COUCHDB_USER="${VITE_COUCHDB_USER:-admin}" \ +--build-arg VITE_COUCHDB_PASSWORD="${VITE_COUCHDB_PASSWORD:-change-this-secure-password}" \ +--build-arg APP_BASE_URL="${APP_BASE_URL:-http://localhost:8080}" \ +--build-arg VITE_GOOGLE_CLIENT_ID="${VITE_GOOGLE_CLIENT_ID:-}" \ +--build-arg VITE_GITHUB_CLIENT_ID="${VITE_GITHUB_CLIENT_ID:-}" \ +--build-arg MAILGUN_API_KEY="${MAILGUN_API_KEY:-}" \ +--build-arg MAILGUN_DOMAIN="${MAILGUN_DOMAIN:-}" \ +--build-arg MAILGUN_FROM_EMAIL="${MAILGUN_FROM_EMAIL:-}" \ +--build-arg NODE_ENV="${NODE_ENV:-production}" \ +-t meds-validation \ +--load \ +. + +print_success "Docker image built successfully" + +print_status "5. Testing container startup and health..." + +# Run container in background +docker run --rm -d \ +-p 8083:80 \ +--name meds-validation-test \ +meds-validation + +# Wait for container to start +sleep 5 + +# Check if container is running +if ! docker ps | grep -q meds-validation-test; then + print_error "Container failed to start" + docker logs meds-validation-test + exit 1 +fi + +print_success "Container started successfully" + +# Test health endpoint +print_status "5. Testing health endpoint..." +for i in {1..10}; do + if curl -s -f http://localhost:8083/health > /dev/null; then + print_success "Health endpoint responding" + break + elif [[ $i -eq 10 ]]; then + print_error "Health endpoint not responding after 10 attempts" + exit 1 + else + print_warning "Health endpoint not ready, retrying... ($i/10)" + sleep 2 + fi +done + +# Test main application +print_status "6. Testing main application..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8083) +if [[ $HTTP_CODE -eq 200 ]]; then + print_success "Main application responding (HTTP $HTTP_CODE)" +else + print_error "Main application not responding properly (HTTP $HTTP_CODE)" + exit 1 +fi + +# Test docker-compose build +print_status "7. Testing Docker Compose build..." +docker compose -f docker/docker-compose.yaml build frontend --no-cache + +print_success "Docker Compose build successful" + +# Test docker-compose with validation project name +print_status "8. Testing Docker Compose deployment..." +docker compose -f docker/docker-compose.yaml -p meds-validation up -d --build + +# Wait for services to start +sleep 10 + +# Check service health +if docker compose -f docker/docker-compose.yaml -p meds-validation ps | grep -q "Up"; then + print_success "Docker Compose services started successfully" +else + print_error "Docker Compose services failed to start" + docker compose -f docker/docker-compose.yaml -p meds-validation logs + exit 1 +fi + +# Test health of compose deployment +if curl -s -f http://localhost:8080/health > /dev/null; then + print_success "Docker Compose health endpoint responding" +else + print_warning "Docker Compose health endpoint not responding (may need CouchDB)" +fi + +print_status "9. Checking image size..." +IMAGE_SIZE=$(docker image inspect meds-validation --format='{{.Size}}' | numfmt --to=iec) +print_success "Image size: $IMAGE_SIZE" + +print_status "10. Validating security configuration..." + +# Check if image runs as non-root +USER_INFO=$(docker run --rm meds-validation whoami) +if [[ "$USER_INFO" != "root" ]]; then + print_success "Container runs as non-root user: $USER_INFO" +else + print_warning "Container runs as root user (security consideration)" +fi + +# Check nginx configuration +if docker run --rm meds-validation nginx -t 2>/dev/null; then + print_success "Nginx configuration is valid" +else + print_error "Nginx configuration has issues" + exit 1 +fi + +print_status "11. Final validation complete!" + +echo +echo "🎉 Deployment validation successful!" +echo +echo "Summary:" +echo "✅ Environment files validated" +echo "✅ Docker image builds successfully" +echo "✅ Container starts and runs healthy" +echo "✅ Health endpoints respond correctly" +echo "✅ Docker Compose deployment works" +echo "✅ Security configuration validated" +echo "✅ Image size optimized ($IMAGE_SIZE)" +echo +echo "Your deployment is ready for production! 🚀" +echo +echo "Next steps:" +echo "1. Configure production environment variables in .env" +echo "2. Run './deploy.sh production' for production deployment" +echo "3. Set up monitoring and backups" +echo "4. Configure SSL/TLS certificates" +echo diff --git a/scripts/validate-env.sh b/scripts/validate-env.sh new file mode 100755 index 0000000..b572b22 --- /dev/null +++ b/scripts/validate-env.sh @@ -0,0 +1,274 @@ +#!/bin/bash + +# Environment validation script +# Validates all .env files for consistency and completeness + +set -e + +print_header() { + echo "🔍 Environment Configuration Validation" + echo "======================================" + echo "" +} + +print_section() { + echo "" + echo "📋 $1" + echo "$(printf '%*s' ${#1} '' | tr ' ' '-')" +} + +print_success() { + echo "✅ $1" +} + +print_warning() { + echo "⚠️ $1" +} + +print_error() { + echo "❌ $1" +} + +# Required variables for each environment +CORE_VARS=( + "COUCHDB_USER" + "COUCHDB_PASSWORD" + "VITE_COUCHDB_URL" + "VITE_COUCHDB_USER" + "VITE_COUCHDB_PASSWORD" + "APP_BASE_URL" + "MAILGUN_API_KEY" + "MAILGUN_DOMAIN" + "MAILGUN_FROM_EMAIL" +) + +K8S_VARS=( + "INGRESS_HOST" +) + +OPTIONAL_VARS=( + "NODE_ENV" + "VITE_GOOGLE_CLIENT_ID" + "VITE_GITHUB_CLIENT_ID" +) + +validate_file() { + local file="$1" + local file_type="$2" + + print_section "Validating $file ($file_type)" + + if [[ ! -f "$file" ]]; then + print_error "File not found: $file" + return 1 + fi + + local missing_vars=() + local found_vars=() + + # Check core variables + for var in "${CORE_VARS[@]}"; do + if grep -q "^${var}=" "$file" || grep -q "^#.*${var}=" "$file"; then + found_vars+=("$var") + else + missing_vars+=("$var") + fi + done + + # Check K8s variables for relevant files + if [[ "$file_type" != "template" ]]; then + for var in "${K8S_VARS[@]}"; do + if grep -q "^${var}=" "$file" || grep -q "^#.*${var}=" "$file"; then + found_vars+=("$var") + else + missing_vars+=("$var") + fi + done + fi + + # Report results + print_success "Found ${#found_vars[@]} variables" + + if [[ ${#missing_vars[@]} -gt 0 ]]; then + print_warning "Missing variables:" + for var in "${missing_vars[@]}"; do + echo " - $var" + done + fi + + # Check for old VITE_MAILGUN variables + if grep -q "VITE_MAILGUN" "$file"; then + print_error "Found deprecated VITE_MAILGUN variables (should be MAILGUN_*)" + fi + + # Check variable format + local malformed_vars=() + while IFS= read -r line; do + if [[ "$line" =~ ^[A-Z_]+=.* ]]; then + local var_name="${line%%=*}" + if [[ ! "$var_name" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then + malformed_vars+=("$var_name") + fi + fi + done < "$file" + + if [[ ${#malformed_vars[@]} -gt 0 ]]; then + print_warning "Malformed variable names:" + for var in "${malformed_vars[@]}"; do + echo " - $var" + done + fi + + echo "" +} + +validate_consistency() { + print_section "Cross-file Consistency Check" + + # Extract variable names from each file + local example_vars=() + local env_vars=() + local prod_vars=() + + if [[ -f ".env.example" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ ^[A-Z_]+=.* ]]; then + example_vars+=("${line%%=*}") + fi + done < ".env.example" + fi + + if [[ -f ".env" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ ^[A-Z_]+=.* ]]; then + env_vars+=("${line%%=*}") + fi + done < ".env" + fi + + if [[ -f ".env.production" ]]; then + while IFS= read -r line; do + if [[ "$line" =~ ^[A-Z_]+=.* ]]; then + prod_vars+=("${line%%=*}") + fi + done < ".env.production" + fi + + # Check if .env and .env.production have all variables from .env.example + local missing_in_env=() + local missing_in_prod=() + + for var in "${example_vars[@]}"; do + if [[ ! " ${env_vars[@]} " =~ " ${var} " ]]; then + missing_in_env+=("$var") + fi + if [[ ! " ${prod_vars[@]} " =~ " ${var} " ]]; then + missing_in_prod+=("$var") + fi + done + + if [[ ${#missing_in_env[@]} -eq 0 ]]; then + print_success ".env has all variables from .env.example" + else + print_warning ".env missing variables from .env.example:" + for var in "${missing_in_env[@]}"; do + echo " - $var" + done + fi + + if [[ ${#missing_in_prod[@]} -eq 0 ]]; then + print_success ".env.production has all variables from .env.example" + else + print_warning ".env.production missing variables from .env.example:" + for var in "${missing_in_prod[@]}"; do + echo " - $var" + done + fi + + echo "" +} + +validate_k8s_template() { + print_section "Kubernetes Template Validation" + + local template_file="k8s/ingress.yaml.template" + + if [[ ! -f "$template_file" ]]; then + print_error "Template file not found: $template_file" + return 1 + fi + + # Check for template variables + local template_vars=() + while IFS= read -r line; do + if [[ "$line" =~ \$\{([A-Z_][A-Z0-9_]*)\} ]]; then + local var_name="${BASH_REMATCH[1]}" + if [[ ! " ${template_vars[@]} " =~ " ${var_name} " ]]; then + template_vars+=("$var_name") + fi + fi + done < "$template_file" + + print_success "Found ${#template_vars[@]} template variables:" + for var in "${template_vars[@]}"; do + echo " - \${$var}" + done + + # Check if template variables are defined in env files + for var in "${template_vars[@]}"; do + local found_in_files=() + + if grep -q "^${var}=" ".env.example" 2>/dev/null; then + found_in_files+=(".env.example") + fi + if grep -q "^${var}=" ".env" 2>/dev/null; then + found_in_files+=(".env") + fi + if grep -q "^${var}=" ".env.production" 2>/dev/null; then + found_in_files+=(".env.production") + fi + + if [[ ${#found_in_files[@]} -gt 0 ]]; then + print_success "$var defined in: ${found_in_files[*]}" + else + print_error "$var not defined in any environment file" + fi + done + + echo "" +} + +main() { + print_header + + # Validate individual files + if [[ -f ".env.example" ]]; then + validate_file ".env.example" "template" + fi + + if [[ -f ".env" ]]; then + validate_file ".env" "development" + fi + + if [[ -f ".env.production" ]]; then + validate_file ".env.production" "production" + fi + + # Cross-file validation + validate_consistency + + # Kubernetes template validation + validate_k8s_template + + print_section "Summary" + print_success "Environment validation complete!" + echo "" + echo "💡 Tips:" + echo " - Copy .env.example to .env for local development" + echo " - Use .env.production for production deployments" + echo " - Run './deploy-k8s.sh --dry-run' to test Kubernetes deployment" + echo " - All Mailgun variables use server-side naming (no VITE_ prefix)" + echo "" +} + +main "$@" diff --git a/services/auth/__tests__/auth.integration.test.ts b/services/auth/__tests__/auth.integration.test.ts new file mode 100644 index 0000000..e4af9f9 --- /dev/null +++ b/services/auth/__tests__/auth.integration.test.ts @@ -0,0 +1,59 @@ +import { authService } from '../auth.service'; +import { AccountStatus } from '../auth.constants'; +import { User } from '../../../types'; + +// Helper to clear localStorage and reset the mock DB before each test +beforeEach(() => { + // Clear all localStorage keys used by dbService and authService + Object.keys(localStorage).forEach(key => localStorage.removeItem(key)); +}); + +describe('Authentication Integration Tests', () => { + const username = 'testuser'; + const password = 'Passw0rd!'; + const email = 'testuser@example.com'; + + test('User registration creates a pending account', async () => { + const result = await authService.register(email, password, username); + expect(result).toBeDefined(); + expect(result.user.username).toBe(username); + expect(result.user.email).toBe(email); + expect(result.user.status).toBe(AccountStatus.PENDING); + expect(result.user.emailVerified).toBeFalsy(); + }); + + test('Login fails for unverified (pending) account', async () => { + await expect(authService.login({ email, password })).rejects.toThrow(); + }); + + test('Email verification activates the account', async () => { + // Register a user first to get the verification token + const result = await authService.register(email, password, username); + const verificationToken = result.verificationToken.token; + + const verifiedUser = await authService.verifyEmail(verificationToken); + expect(verifiedUser.status).toBe(AccountStatus.ACTIVE); + expect(verifiedUser.emailVerified).toBeTruthy(); + }); + + test('Login succeeds after email verification', async () => { + const tokens = await authService.login({ email, password }); + expect(tokens).toBeDefined(); + expect(tokens.accessToken).toBeTruthy(); + expect(tokens.refreshToken).toBeTruthy(); + }); + + test('OAuth flow registers or logs in a user', async () => { + const oauthEmail = 'oauthuser@example.com'; + const oauthName = 'OAuth User'; + const result = await authService.loginWithOAuth('google', { + email: oauthEmail, + username: oauthName, + }); + expect(result).toBeDefined(); + expect(result.user.email).toBe(oauthEmail); + // OAuth users should be active immediately + expect(result.user.status).toBe(AccountStatus.ACTIVE); + expect(result.user.emailVerified).toBeTruthy(); + }); +}); diff --git a/services/auth/__tests__/emailVerification.test.ts b/services/auth/__tests__/emailVerification.test.ts new file mode 100644 index 0000000..bbd0af6 --- /dev/null +++ b/services/auth/__tests__/emailVerification.test.ts @@ -0,0 +1,59 @@ +import { EmailVerificationService } from '../emailVerification.service'; +import { dbService } from '../../couchdb.factory'; + +jest.mock('../../couchdb.factory'); +jest.mock('../../email'); + +describe('EmailVerificationService', () => { + let emailVerificationService: EmailVerificationService; + + beforeEach(() => { + emailVerificationService = new EmailVerificationService(); + }); + + test('should generate and validate verification token', async () => { + const user = { + _id: 'user1', + email: 'test@example.com', + username: 'testuser', + password: 'password', + }; + + const verificationToken = + await emailVerificationService.generateVerificationToken(user as any); + + expect(verificationToken).toBeDefined(); + expect(verificationToken.token).toBeDefined(); + expect(verificationToken.expiresAt).toBeDefined(); + + const validatedUser = + await emailVerificationService.validateVerificationToken( + verificationToken.token + ); + + expect(validatedUser).toBeDefined(); + expect(validatedUser!._id).toBe(user._id); + }); + + test('should not validate expired token', async () => { + const user = { + _id: 'user2', + email: 'test2@example.com', + username: 'testuser2', + password: 'password2', + }; + + const verificationToken = + await emailVerificationService.generateVerificationToken(user as any); + + // Set expiresAt to past date + verificationToken.expiresAt = new Date(Date.now() - 1000 * 60 * 60 * 24); + + const validatedUser = + await emailVerificationService.validateVerificationToken( + verificationToken.token + ); + + expect(validatedUser).toBeNull(); + }); +}); diff --git a/services/auth/auth.constants.ts b/services/auth/auth.constants.ts new file mode 100644 index 0000000..81234d3 --- /dev/null +++ b/services/auth/auth.constants.ts @@ -0,0 +1,25 @@ +// Client-side auth constants for demo/development purposes +// In production, these would be handled by a secure backend service + +export const JWT_EXPIRES_IN = '1h'; +export const REFRESH_TOKEN_EXPIRES_IN = '7d'; +export const EMAIL_VERIFICATION_EXPIRES_IN = '24h'; + +// Mock secrets for frontend-only demo (NOT for production use) +export const JWT_SECRET = 'demo_jwt_secret_for_frontend_only'; +export const REFRESH_TOKEN_SECRET = 'demo_refresh_secret_for_frontend_only'; +export const EMAIL_VERIFICATION_SECRET = + 'demo_email_verification_secret_for_frontend_only'; + +export enum AccountStatus { + PENDING = 'PENDING', + ACTIVE = 'ACTIVE', + SUSPENDED = 'SUSPENDED', +} + +export interface AuthConfig { + jwtSecret: string; + jwtExpiresIn: string; + refreshTokenExpiresIn: string; + emailVerificationExpiresIn: string; +} diff --git a/services/auth/auth.error.ts b/services/auth/auth.error.ts new file mode 100644 index 0000000..bddbc03 --- /dev/null +++ b/services/auth/auth.error.ts @@ -0,0 +1,43 @@ +import { NextFunction, Request, Response } from 'express'; + +/** + * Custom AuthError class that extends Error with HTTP status code + * Security: Provides consistent error handling for authentication issues + */ +export class AuthError extends Error { + statusCode: number; + + constructor(message: string, statusCode: number = 401) { + super(message); + this.statusCode = statusCode; + this.name = 'AuthError'; + } +} + +/** + * Middleware to handle AuthError exceptions + * Security: Centralized error handling for authentication errors + */ +export const handleAuthError = ( + err: Error, + req: Request, + res: Response, + next: NextFunction +) => { + if (err instanceof AuthError) { + return res.status(err.statusCode).json({ + error: err.message, + statusCode: err.statusCode, + }); + } + + // Handle JWT verification errors + if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') { + return res.status(401).json({ + error: 'Invalid or expired token', + statusCode: 401, + }); + } + + next(err); +}; diff --git a/services/auth/auth.middleware.ts b/services/auth/auth.middleware.ts new file mode 100644 index 0000000..cc53207 --- /dev/null +++ b/services/auth/auth.middleware.ts @@ -0,0 +1,48 @@ +import { Request, Response, NextFunction } from 'express'; +import * as jwt from 'jsonwebtoken'; +import { JWT_SECRET } from './auth.constants'; +import { AuthError, handleAuthError } from './auth.error'; + +// Security: JWT authentication middleware +export const authenticate = ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + // Security: Get token from Authorization header + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new AuthError('No authentication token provided', 401); + } + + const token = authHeader.split(' ')[1]; + + // Security: Verify JWT token + const decoded = jwt.verify(token, JWT_SECRET); + + // Add user information to request + req.user = decoded; + + next(); + } catch (error) { + handleAuthError(error, req, res, next); + } +}; + +// Security: Role-based authorization middleware +export const authorize = (...allowedRoles: string[]) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + // Security: Check if user exists in request + if (!req.user) { + throw new AuthError('Authentication required', 401); + } + + // In a full implementation, we would check user roles + next(); + } catch (error) { + handleAuthError(error, req, res, next); + } + }; +}; diff --git a/services/auth/auth.service.ts b/services/auth/auth.service.ts new file mode 100644 index 0000000..1804279 --- /dev/null +++ b/services/auth/auth.service.ts @@ -0,0 +1,244 @@ +import { v4 as uuidv4 } from 'uuid'; +import { dbService } from '../../services/couchdb.factory'; +import { AccountStatus } from './auth.constants'; +import { User } from '../../types'; +import { AuthenticatedUser } from './auth.types'; +import { EmailVerificationService } from './emailVerification.service'; + +const emailVerificationService = new EmailVerificationService(); + +const authService = { + async register(email: string, password: string, username?: string) { + try { + // Create user with password + const user = await dbService.createUserWithPassword( + email, + password, + username + ); + + // Generate and send verification token (in production) + const verificationToken = + await emailVerificationService.generateVerificationToken( + user as AuthenticatedUser + ); + + return { user, verificationToken }; + } catch (error) { + if (error.message.includes('already exists')) { + throw new Error('An account with this email already exists'); + } + throw error; + } + }, + + async login(input: { email: string; password: string }) { + console.log('🔐 Login attempt for:', input.email); + + // Find user by email + const user = await dbService.findUserByEmail(input.email); + + if (!user) { + console.log('❌ User not found for email:', input.email); + throw new Error('User not found'); + } + + console.log('👤 User found:', { + email: user.email, + hasPassword: !!user.password, + role: user.role, + status: user.status, + emailVerified: user.emailVerified, + }); + + // Check if user has a password (email-based account) + if (!user.password) { + console.log('❌ No password found - OAuth account'); + throw new Error( + 'This account was created with OAuth. Please use Google or GitHub to sign in.' + ); + } + + // Simple password verification (in production, use bcrypt) + console.log('🔍 Comparing passwords:', { + inputPassword: input.password, + storedPassword: user.password, + match: user.password === input.password, + }); + + if (user.password !== input.password) { + console.log('❌ Password mismatch'); + throw new Error('Invalid password'); + } + + console.log('✅ Login successful for:', user.email); + + // Return mock tokens for frontend compatibility + return { + user, + accessToken: 'mock_access_token_' + Date.now(), + refreshToken: 'mock_refresh_token_' + Date.now(), + }; + }, + + async loginWithOAuth( + provider: 'google' | 'github', + userData: { email: string; username: string; avatar?: string } + ) { + try { + // Try to find existing user by email + let user = await dbService.findUserByEmail(userData.email); + + if (!user) { + // Create new user from OAuth data + user = await dbService.createUserFromOAuth(userData); + } + + // Generate access tokens + return { + user, + accessToken: `oauth_${provider}_token_` + Date.now(), + refreshToken: `oauth_${provider}_refresh_` + Date.now(), + }; + } catch (error) { + throw new Error(`OAuth login failed: ${error.message}`); + } + }, + + async verifyEmail(token: string) { + const user = + await emailVerificationService.validateVerificationToken(token); + + if (!user) { + throw new Error('Invalid or expired verification token'); + } + + await emailVerificationService.markEmailVerified(user); + + return user; + }, + + async changePassword( + userId: string, + currentPassword: string, + newPassword: string + ) { + // Get user by ID + const user = await dbService.getUserById(userId); + + if (!user) { + throw new Error('User not found'); + } + + // Check if user has a password (not OAuth user) + if (!user.password) { + throw new Error('Cannot change password for OAuth accounts'); + } + + // Verify current password + if (user.password !== currentPassword) { + throw new Error('Current password is incorrect'); + } + + // Validate new password + if (newPassword.length < 6) { + throw new Error('New password must be at least 6 characters long'); + } + + // Update password + const updatedUser = await dbService.changeUserPassword(userId, newPassword); + + return { + user: updatedUser, + message: 'Password changed successfully', + }; + }, + + async requestPasswordReset(email: string) { + const user = await dbService.findUserByEmail(email); + + if (!user) { + // Don't reveal if email exists or not for security + return { + message: + 'If an account with this email exists, a password reset link has been sent.', + }; + } + + if (!user.password) { + throw new Error('Cannot reset password for OAuth accounts'); + } + + // Generate reset token (similar to verification token) + const resetToken = uuidv4().replace(/-/g, ''); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 1); // 1 hour expiry + + // Store reset token (in production, save to database) + const resetTokens = JSON.parse( + localStorage.getItem('password_reset_tokens') || '[]' + ); + resetTokens.push({ + userId: user._id, + email: user.email, + token: resetToken, + expiresAt, + }); + localStorage.setItem('password_reset_tokens', JSON.stringify(resetTokens)); + + // Send reset email + const emailSent = await emailVerificationService.sendPasswordResetEmail( + user.email!, + resetToken + ); + + return { + message: + 'If an account with this email exists, a password reset link has been sent.', + emailSent, + }; + }, + + async resetPassword(token: string, newPassword: string) { + // Get reset tokens + const resetTokens = JSON.parse( + localStorage.getItem('password_reset_tokens') || '[]' + ); + const resetToken = resetTokens.find((t: any) => t.token === token); + + if (!resetToken) { + throw new Error('Invalid or expired reset token'); + } + + // Check if token is expired + if (new Date() > new Date(resetToken.expiresAt)) { + throw new Error('Reset token has expired'); + } + + // Validate new password + if (newPassword.length < 6) { + throw new Error('Password must be at least 6 characters long'); + } + + // Update password + const updatedUser = await dbService.changeUserPassword( + resetToken.userId, + newPassword + ); + + // Remove used token + const filteredTokens = resetTokens.filter((t: any) => t.token !== token); + localStorage.setItem( + 'password_reset_tokens', + JSON.stringify(filteredTokens) + ); + + return { + user: updatedUser, + message: 'Password reset successfully', + }; + }, +}; + +export { authService }; +export default authService; diff --git a/services/auth/auth.types.ts b/services/auth/auth.types.ts new file mode 100644 index 0000000..774d152 --- /dev/null +++ b/services/auth/auth.types.ts @@ -0,0 +1,42 @@ +import { User } from '../../types'; +import { AccountStatus } from './auth.constants'; + +export interface RegisterInput { + username: string; + email: string; + password: string; +} + +export interface LoginInput { + email: string; + password: string; +} + +export interface AuthResponse { + user: User; + accessToken: string; + refreshToken: string; +} + +export interface TokenPayload { + userId: string; + username: string; +} + +export interface EmailVerificationToken { + userId: string; + email: string; + token: string; + expiresAt: Date; +} + +export interface RefreshTokenPayload { + userId: string; + refreshToken: string; +} + +export interface AuthenticatedUser extends User { + status: AccountStatus; + email?: string; + emailVerified?: boolean; +} diff --git a/services/auth/emailVerification.service.ts b/services/auth/emailVerification.service.ts new file mode 100644 index 0000000..0ff9978 --- /dev/null +++ b/services/auth/emailVerification.service.ts @@ -0,0 +1,95 @@ +import { v4 as uuidv4 } from 'uuid'; +import { EmailVerificationToken, AuthenticatedUser } from './auth.types'; +import { mailgunService } from '../mailgun.service'; +import { AccountStatus } from './auth.constants'; + +const TOKEN_EXPIRY_HOURS = 24; + +export class EmailVerificationService { + async generateVerificationToken( + user: AuthenticatedUser + ): Promise { + const token = uuidv4().replace(/-/g, ''); // Generate a random token using UUID + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + TOKEN_EXPIRY_HOURS); + + const verificationToken: EmailVerificationToken = { + userId: user._id, + email: user.email || '', + token, + expiresAt, + }; + + // Store token in localStorage for demo (in production, save to database) + const tokens = JSON.parse( + localStorage.getItem('verification_tokens') || '[]' + ); + tokens.push(verificationToken); + localStorage.setItem('verification_tokens', JSON.stringify(tokens)); + + // Send verification email via Mailgun + if (user.email) { + const emailSent = await mailgunService.sendVerificationEmail( + user.email, + token + ); + if (!emailSent) { + console.warn('Failed to send verification email'); + } + } + + return verificationToken; + } + + async validateVerificationToken( + token: string + ): Promise { + // Get tokens from localStorage + const tokens = JSON.parse( + localStorage.getItem('verification_tokens') || '[]' + ); + const verificationToken = tokens.find( + (t: EmailVerificationToken) => t.token === token + ); + + if (!verificationToken) { + return null; + } + + // Check if token is expired + if (new Date() > new Date(verificationToken.expiresAt)) { + return null; + } + + // Find the user (in production, this would be a proper database lookup) + const { dbService } = await import('../couchdb'); + const user = await dbService.findUserByEmail(verificationToken.email); + + return user as AuthenticatedUser; + } + + async markEmailVerified(user: AuthenticatedUser): Promise { + // Update user in database + const { dbService } = await import('../couchdb'); + const updatedUser = { + ...user, + emailVerified: true, + status: AccountStatus.ACTIVE, + }; + + await dbService.updateUser(updatedUser); + + // Remove used token + const tokens = JSON.parse( + localStorage.getItem('verification_tokens') || '[]' + ); + const filteredTokens = tokens.filter( + (t: EmailVerificationToken) => t.userId !== user._id + ); + localStorage.setItem('verification_tokens', JSON.stringify(filteredTokens)); + } + + async sendPasswordResetEmail(email: string, token: string): Promise { + return mailgunService.sendPasswordResetEmail(email, token); + } +} diff --git a/services/auth/templates/verification.email.ts b/services/auth/templates/verification.email.ts new file mode 100644 index 0000000..c26b519 --- /dev/null +++ b/services/auth/templates/verification.email.ts @@ -0,0 +1,17 @@ +import { EmailVerificationToken } from '../auth.types'; + +export const verificationEmailTemplate = (token: EmailVerificationToken) => { + const baseUrl = process.env.APP_BASE_URL || 'http://localhost:5173'; + const verificationLink = `${baseUrl}/verify-email?token=${token.token}`; + + return ` + + +

    Email Verification

    +

    Please verify your email address by clicking the link below:

    +

    ${verificationLink}

    +

    This link will expire in 24 hours.

    + + + `; +}; diff --git a/services/couchdb.factory.ts b/services/couchdb.factory.ts new file mode 100644 index 0000000..a9159f2 --- /dev/null +++ b/services/couchdb.factory.ts @@ -0,0 +1,44 @@ +// Production CouchDB Service Configuration +// This file determines whether to use mock localStorage or real CouchDB + +import { CouchDBService as MockCouchDBService } from './couchdb'; + +// Environment detection +const isProduction = () => { + // Check if we're in a Docker environment or if CouchDB URL is configured + const env = (import.meta as any).env || {}; + const couchdbUrl = + env.VITE_COUCHDB_URL || + (typeof process !== 'undefined' ? process.env.VITE_COUCHDB_URL : null) || + (typeof process !== 'undefined' ? process.env.COUCHDB_URL : null); + + return !!couchdbUrl && couchdbUrl !== 'mock'; +}; + +// Create the database service based on environment +const createDbService = () => { + if (isProduction()) { + try { + // Use dynamic require to avoid TypeScript resolution issues + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { + CouchDBService: RealCouchDBService, + } = require('./couchdb.production'); + return new RealCouchDBService(); + } catch (error) { + console.warn( + 'Production CouchDB service not available, falling back to mock:', + error + ); + return new MockCouchDBService(); + } + } else { + return new MockCouchDBService(); + } +}; + +// Export the database service instance +export const dbService = createDbService(); + +// Re-export the error class +export { CouchDBError } from './couchdb'; diff --git a/services/couchdb.production.ts b/services/couchdb.production.ts new file mode 100644 index 0000000..aadb401 --- /dev/null +++ b/services/couchdb.production.ts @@ -0,0 +1,392 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + User, + Medication, + UserSettings, + TakenDoses, + CustomReminder, + CouchDBDocument, + UserRole, +} from '../types'; +import { AccountStatus } from './auth/auth.constants'; +import { CouchDBError } from './couchdb'; + +// Production CouchDB Service that connects to a real CouchDB instance +export class CouchDBService { + private baseUrl: string; + private auth: string; + + constructor() { + // Get CouchDB configuration from environment + const couchdbUrl = + (import.meta as any).env?.VITE_COUCHDB_URL || 'http://localhost:5984'; + const couchdbUser = (import.meta as any).env?.VITE_COUCHDB_USER || 'admin'; + const couchdbPassword = + (import.meta as any).env?.VITE_COUCHDB_PASSWORD || 'password'; + + this.baseUrl = couchdbUrl; + this.auth = btoa(`${couchdbUser}:${couchdbPassword}`); + + // Initialize databases + this.initializeDatabases(); + } + + private async initializeDatabases(): Promise { + const databases = [ + 'users', + 'medications', + 'settings', + 'taken_doses', + 'reminders', + ]; + + for (const dbName of databases) { + try { + await this.createDatabaseIfNotExists(dbName); + } catch (error) { + console.error(`Failed to initialize database ${dbName}:`, error); + } + } + } + + private async createDatabaseIfNotExists(dbName: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/${dbName}`, { + method: 'HEAD', + headers: { + Authorization: `Basic ${this.auth}`, + }, + }); + + if (response.status === 404) { + // Database doesn't exist, create it + const createResponse = await fetch(`${this.baseUrl}/${dbName}`, { + method: 'PUT', + headers: { + Authorization: `Basic ${this.auth}`, + 'Content-Type': 'application/json', + }, + }); + + if (!createResponse.ok) { + throw new Error(`Failed to create database ${dbName}`); + } + + console.log(`✅ Created CouchDB database: ${dbName}`); + } + } catch (error) { + console.error(`Error checking/creating database ${dbName}:`, error); + throw error; + } + } + + private async makeRequest( + method: string, + path: string, + body?: any + ): Promise { + const url = `${this.baseUrl}${path}`; + const options: RequestInit = { + method, + headers: { + Authorization: `Basic ${this.auth}`, + 'Content-Type': 'application/json', + }, + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const errorText = await response.text(); + throw new CouchDBError(`CouchDB error: ${errorText}`, response.status); + } + + return response.json(); + } + + private async getDoc( + dbName: string, + id: string + ): Promise { + try { + const doc = await this.makeRequest('GET', `/${dbName}/${id}`); + return doc; + } catch (error) { + if (error instanceof CouchDBError && error.status === 404) { + return null; + } + throw error; + } + } + + private async putDoc( + dbName: string, + doc: Omit & { _rev?: string } + ): Promise { + const response = await this.makeRequest( + 'PUT', + `/${dbName}/${doc._id}`, + doc + ); + return { ...doc, _rev: response.rev } as T; + } + + private async query(dbName: string, selector: any): Promise { + const response = await this.makeRequest('POST', `/${dbName}/_find`, { + selector, + limit: 1000, + }); + return response.docs; + } + + // User Management Methods + async findUserByUsername(username: string): Promise { + const users = await this.query('users', { username }); + return users[0] || null; + } + + async findUserByEmail(email: string): Promise { + const users = await this.query('users', { email }); + return users[0] || null; + } + + async createUser(username: string): Promise { + const existingUser = await this.findUserByUsername(username); + if (existingUser) { + throw new CouchDBError('User already exists', 409); + } + + const newUser: Omit = { _id: uuidv4(), username }; + return this.putDoc('users', newUser); + } + + async createUserWithPassword( + email: string, + password: string, + username?: string + ): Promise { + const existingUser = await this.findUserByEmail(email); + if (existingUser) { + throw new CouchDBError('User already exists', 409); + } + + const newUser: Omit = { + _id: uuidv4(), + username: username || email.split('@')[0], + email, + password, + emailVerified: false, + status: AccountStatus.PENDING, + role: UserRole.USER, + createdAt: new Date(), + lastLoginAt: new Date(), + }; + + return this.putDoc('users', newUser); + } + + async createUserFromOAuth(userData: { + email: string; + username: string; + avatar?: string; + }): Promise { + const existingUser = await this.findUserByEmail(userData.email); + if (existingUser) { + throw new CouchDBError('User already exists', 409); + } + + const newUser: Omit = { + _id: uuidv4(), + username: userData.username, + email: userData.email, + avatar: userData.avatar, + emailVerified: true, + status: AccountStatus.ACTIVE, + role: UserRole.USER, + createdAt: new Date(), + lastLoginAt: new Date(), + }; + + return this.putDoc('users', newUser); + } + + async getUserById(id: string): Promise { + return this.getDoc('users', id); + } + + async updateUser(user: User): Promise { + return this.putDoc('users', user); + } + + async deleteUser(id: string): Promise { + const user = await this.getDoc('users', id); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + await this.makeRequest('DELETE', `/users/${id}?rev=${user._rev}`); + } + + // Medication Methods + async getMedications(userId: string): Promise { + return this.query('medications', { userId }); + } + + async createMedication( + medication: Omit + ): Promise { + const newMedication = { ...medication, _id: uuidv4() }; + return this.putDoc('medications', newMedication); + } + + async updateMedication(medication: Medication): Promise { + return this.putDoc('medications', medication); + } + + async deleteMedication(id: string): Promise { + const medication = await this.getDoc('medications', id); + if (!medication) { + throw new CouchDBError('Medication not found', 404); + } + + await this.makeRequest( + 'DELETE', + `/medications/${id}?rev=${medication._rev}` + ); + } + + // Settings Methods + async getSettings(userId: string): Promise { + const settings = await this.getDoc('settings', userId); + if (!settings) { + const defaultSettings: Omit = { + _id: userId, + notificationsEnabled: true, + hasCompletedOnboarding: false, + }; + return this.putDoc('settings', defaultSettings); + } + return settings; + } + + async updateSettings(settings: UserSettings): Promise { + return this.putDoc('settings', settings); + } + + // Taken Doses Methods + async getTakenDoses(userId: string): Promise { + const doses = await this.getDoc('taken_doses', userId); + if (!doses) { + const defaultDoses: Omit = { + _id: userId, + doses: {}, + }; + return this.putDoc('taken_doses', defaultDoses); + } + return doses; + } + + async updateTakenDoses(takenDoses: TakenDoses): Promise { + return this.putDoc('taken_doses', takenDoses); + } + + // Reminder Methods + async getReminders(userId: string): Promise { + return this.query('reminders', { userId }); + } + + async createReminder( + reminder: Omit + ): Promise { + const newReminder = { ...reminder, _id: uuidv4() }; + return this.putDoc('reminders', newReminder); + } + + async updateReminder(reminder: CustomReminder): Promise { + return this.putDoc('reminders', reminder); + } + + async deleteReminder(id: string): Promise { + const reminder = await this.getDoc('reminders', id); + if (!reminder) { + throw new CouchDBError('Reminder not found', 404); + } + + await this.makeRequest('DELETE', `/reminders/${id}?rev=${reminder._rev}`); + } + + // Admin Methods + async getAllUsers(): Promise { + return this.query('users', {}); + } + + async updateUserStatus(userId: string, status: AccountStatus): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + const updatedUser = { ...user, status }; + return this.updateUser(updatedUser); + } + + async changeUserPassword(userId: string, newPassword: string): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + const updatedUser = { ...user, password: newPassword }; + return this.updateUser(updatedUser); + } + + // Cleanup Methods + async deleteAllUserData(userId: string): Promise { + // Delete user medications, settings, doses, and reminders + const [medications, reminders] = await Promise.all([ + this.getMedications(userId), + this.getReminders(userId), + ]); + + // Delete all user data + const deletePromises = [ + ...medications.map(med => this.deleteMedication(med._id)), + ...reminders.map(rem => this.deleteReminder(rem._id)), + ]; + + // Delete settings and taken doses + try { + const settings = await this.getDoc('settings', userId); + if (settings) { + deletePromises.push( + this.makeRequest('DELETE', `/settings/${userId}?rev=${settings._rev}`) + ); + } + } catch (error) { + // Settings might not exist + } + + try { + const takenDoses = await this.getDoc('taken_doses', userId); + if (takenDoses) { + deletePromises.push( + this.makeRequest( + 'DELETE', + `/taken_doses/${userId}?rev=${takenDoses._rev}` + ) + ); + } + } catch (error) { + // Taken doses might not exist + } + + await Promise.all(deletePromises); + + // Finally delete the user + await this.deleteUser(userId); + } +} diff --git a/services/couchdb.ts b/services/couchdb.ts new file mode 100644 index 0000000..da404b6 --- /dev/null +++ b/services/couchdb.ts @@ -0,0 +1,402 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + User, + Medication, + UserSettings, + TakenDoses, + CustomReminder, + CouchDBDocument, + UserRole, +} from '../types'; +import { AccountStatus } from './auth/auth.constants'; + +// This is a mock CouchDB service that uses localStorage for persistence. +// It mimics the async nature of a real database API and includes robust error handling and conflict resolution. + +const latency = () => + new Promise(res => setTimeout(res, Math.random() * 200 + 50)); + +export class CouchDBError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.name = 'CouchDBError'; + this.status = status; + } +} + +class CouchDBService { + private async getDb(dbName: string): Promise { + await latency(); + const db = localStorage.getItem(dbName); + return db ? JSON.parse(db) : []; + } + + private async saveDb(dbName: string, data: T[]): Promise { + await latency(); + localStorage.setItem(dbName, JSON.stringify(data)); + } + + private async getDoc( + dbName: string, + id: string + ): Promise { + const allDocs = await this.getDb(dbName); + return allDocs.find(doc => doc._id === id) || null; + } + + private async query( + dbName: string, + predicate: (doc: T) => boolean + ): Promise { + const allDocs = await this.getDb(dbName); + return allDocs.filter(predicate); + } + + private async putDoc( + dbName: string, + doc: Omit & { _rev?: string } + ): Promise { + const allDocs = await this.getDb(dbName); + const docIndex = allDocs.findIndex(d => d._id === doc._id); + + if (docIndex > -1) { + // Update + const existingDoc = allDocs[docIndex]; + if (existingDoc._rev !== doc._rev) { + throw new CouchDBError('Document update conflict', 409); + } + const newRev = parseInt(existingDoc._rev.split('-')[0], 10) + 1; + const updatedDoc = { + ...doc, + _rev: `${newRev}-${Math.random().toString(36).substr(2, 9)}`, + } as T; + allDocs[docIndex] = updatedDoc; + await this.saveDb(dbName, allDocs); + return updatedDoc; + } else { + // Create + const newDoc = { + ...doc, + _rev: `1-${Math.random().toString(36).substr(2, 9)}`, + } as T; + allDocs.push(newDoc); + await this.saveDb(dbName, allDocs); + return newDoc; + } + } + + private async deleteDoc( + dbName: string, + doc: T + ): Promise { + let docs = await this.getDb(dbName); + const docIndex = docs.findIndex(d => d._id === doc._id); + if (docIndex > -1) { + if (docs[docIndex]._rev !== doc._rev) { + throw new CouchDBError('Document update conflict', 409); + } + docs = docs.filter(m => m._id !== doc._id); + await this.saveDb(dbName, docs); + } else { + throw new CouchDBError('Document not found', 404); + } + } + + // Generic update function with conflict resolution + private async updateDocWithConflictResolution( + dbName: string, + doc: T, + mergeFn?: (latest: T, incoming: T) => T + ): Promise { + try { + return await this.putDoc(dbName, doc); + } catch (error) { + if (error instanceof CouchDBError && error.status === 409) { + console.warn( + `Conflict detected for doc ${doc._id}. Attempting to resolve.` + ); + const latestDoc = await this.getDoc(dbName, doc._id); + if (latestDoc) { + // Default merge: incoming changes overwrite latest + const defaultMerge = { ...latestDoc, ...doc, _rev: latestDoc._rev }; + const mergedDoc = mergeFn ? mergeFn(latestDoc, doc) : defaultMerge; + // Retry the update with the latest revision and merged data + return this.putDoc(dbName, mergedDoc); + } + } + // Re-throw if it's not a resolvable conflict or fetching the latest doc fails + throw error; + } + } + + // --- User Management --- + async findUserByUsername(username: string): Promise { + const users = await this.query( + 'users', + u => u.username.toLowerCase() === username.toLowerCase() + ); + return users[0] || null; + } + + async findUserByEmail(email: string): Promise { + const users = await this.query( + 'users', + u => u.email?.toLowerCase() === email.toLowerCase() + ); + return users[0] || null; + } + + async createUser(username: string): Promise { + if (await this.findUserByUsername(username)) { + throw new CouchDBError('User already exists', 409); + } + const newUser: Omit = { _id: uuidv4(), username }; + return this.putDoc('users', newUser); + } + + async createUserWithPassword( + email: string, + password: string, + username?: string + ): Promise { + // Check if user already exists by email + const existingUser = await this.findUserByEmail(email); + if (existingUser) { + throw new CouchDBError('User already exists', 409); + } + + const newUser: Omit = { + _id: uuidv4(), + username: username || email.split('@')[0], // Default username from email + email, + password, // In production, this should be hashed with bcrypt + emailVerified: false, // Require email verification for password accounts + status: AccountStatus.PENDING, + role: UserRole.USER, // Default role is USER + createdAt: new Date(), + lastLoginAt: new Date(), + }; + return this.putDoc('users', newUser); + } + + async createUserFromOAuth(userData: { + email: string; + username: string; + avatar?: string; + }): Promise { + // Check if user already exists by email + const existingUser = await this.findUserByEmail(userData.email); + if (existingUser) { + throw new CouchDBError('User already exists', 409); + } + + const newUser: Omit = { + _id: uuidv4(), + username: userData.username, + email: userData.email, + avatar: userData.avatar, + emailVerified: true, // OAuth users have verified emails + status: AccountStatus.ACTIVE, + role: UserRole.USER, // Default role is USER + createdAt: new Date(), + lastLoginAt: new Date(), + }; + return this.putDoc('users', newUser); + } + + async updateUser(user: User): Promise { + return this.updateDocWithConflictResolution('users', user); + } + + // --- Admin User Management --- + async getAllUsers(): Promise { + return this.getDb('users'); + } + + async getUserById(userId: string): Promise { + return this.getDoc('users', userId); + } + + async suspendUser(userId: string): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + const updatedUser = { ...user, status: AccountStatus.SUSPENDED }; + return this.updateUser(updatedUser); + } + + async activateUser(userId: string): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + const updatedUser = { ...user, status: AccountStatus.ACTIVE }; + return this.updateUser(updatedUser); + } + + async deleteUser(userId: string): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + // Delete user data + await this.deleteDoc('users', user); + + // Delete user's associated data + const userMeds = this.getUserDbName('meds', userId); + const userSettings = this.getUserDbName('settings', userId); + const userTaken = this.getUserDbName('taken', userId); + const userReminders = this.getUserDbName('reminders', userId); + + localStorage.removeItem(userMeds); + localStorage.removeItem(userSettings); + localStorage.removeItem(userTaken); + localStorage.removeItem(userReminders); + } + + async changeUserPassword(userId: string, newPassword: string): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new CouchDBError('User not found', 404); + } + + // In production, hash the password with bcrypt + const updatedUser = { ...user, password: newPassword }; + return this.updateUser(updatedUser); + } + + // --- User Data Management --- + private getUserDbName = ( + type: 'meds' | 'settings' | 'taken' | 'reminders', + userId: string + ) => `${type}_${userId}`; + + async getMedications(userId: string): Promise { + return this.getDb(this.getUserDbName('meds', userId)); + } + + async addMedication( + userId: string, + med: Omit + ): Promise { + const newMed = { ...med, _id: uuidv4() }; + return this.putDoc(this.getUserDbName('meds', userId), newMed); + } + + async updateMedication(userId: string, med: Medication): Promise { + return this.updateDocWithConflictResolution( + this.getUserDbName('meds', userId), + med + ); + } + + async deleteMedication(userId: string, med: Medication): Promise { + return this.deleteDoc(this.getUserDbName('meds', userId), med); + } + + async getCustomReminders(userId: string): Promise { + return this.getDb(this.getUserDbName('reminders', userId)); + } + + async addCustomReminder( + userId: string, + reminder: Omit + ): Promise { + const newReminder = { ...reminder, _id: uuidv4() }; + return this.putDoc( + this.getUserDbName('reminders', userId), + newReminder + ); + } + + async updateCustomReminder( + userId: string, + reminder: CustomReminder + ): Promise { + return this.updateDocWithConflictResolution( + this.getUserDbName('reminders', userId), + reminder + ); + } + + async deleteCustomReminder( + userId: string, + reminder: CustomReminder + ): Promise { + return this.deleteDoc( + this.getUserDbName('reminders', userId), + reminder + ); + } + + async getSettings(userId: string): Promise { + const dbName = this.getUserDbName('settings', userId); + let settings = await this.getDoc(dbName, userId); + if (!settings) { + settings = await this.putDoc(dbName, { + _id: userId, + notificationsEnabled: true, + hasCompletedOnboarding: false, + }); + } + return settings; + } + + async updateSettings( + userId: string, + settings: UserSettings + ): Promise { + return this.updateDocWithConflictResolution( + this.getUserDbName('settings', userId), + settings + ); + } + + async getTakenDoses(userId: string): Promise { + const dbName = this.getUserDbName('taken', userId); + let takenDoses = await this.getDoc(dbName, userId); + if (!takenDoses) { + takenDoses = await this.putDoc(dbName, { + _id: userId, + doses: {}, + }); + } + return takenDoses; + } + + async updateTakenDoses( + userId: string, + takenDoses: TakenDoses + ): Promise { + // Custom merge logic for taken doses to avoid overwriting recent updates + const mergeFn = (latest: TakenDoses, incoming: TakenDoses): TakenDoses => { + return { + ...latest, // Use latest doc as the base + ...incoming, // Apply incoming changes + doses: { ...latest.doses, ...incoming.doses }, // Specifically merge the doses object + _rev: latest._rev, // IMPORTANT: Use the latest revision for the update attempt + }; + }; + return this.updateDocWithConflictResolution( + this.getUserDbName('taken', userId), + takenDoses, + mergeFn + ); + } + + async deleteAllUserData(userId: string): Promise { + await latency(); + localStorage.removeItem(this.getUserDbName('meds', userId)); + localStorage.removeItem(this.getUserDbName('settings', userId)); + localStorage.removeItem(this.getUserDbName('taken', userId)); + localStorage.removeItem(this.getUserDbName('reminders', userId)); + } +} + +export { CouchDBService }; +export const dbService = new CouchDBService(); diff --git a/services/database.seeder.ts b/services/database.seeder.ts new file mode 100644 index 0000000..d67ab06 --- /dev/null +++ b/services/database.seeder.ts @@ -0,0 +1,101 @@ +import { dbService } from './couchdb.factory'; +import { AccountStatus } from './auth/auth.constants'; +import { UserRole } from '../types'; + +export class DatabaseSeeder { + private static seedingInProgress = false; + private static seedingCompleted = false; + + async seedDefaultAdmin(): Promise { + const adminEmail = 'admin@localhost'; + const adminPassword = 'admin123!'; + + console.log('🌱 Starting admin user seeding...'); + console.log('📧 Admin email:', adminEmail); + + try { + // Check if admin already exists + const existingAdmin = await dbService.findUserByEmail(adminEmail); + + if (existingAdmin) { + console.log('✅ Default admin user already exists'); + console.log('👤 Existing admin:', existingAdmin); + + // Check if admin needs to be updated to correct role/status + if ( + existingAdmin.role !== UserRole.ADMIN || + existingAdmin.status !== AccountStatus.ACTIVE + ) { + console.log('🔧 Updating admin user role and status...'); + const updatedAdmin = { + ...existingAdmin, + role: UserRole.ADMIN, + status: AccountStatus.ACTIVE, + emailVerified: true, + }; + await dbService.updateUser(updatedAdmin); + console.log('✅ Admin user updated successfully'); + console.log('👤 Updated admin:', updatedAdmin); + } + return; + } + + console.log('🔨 Creating new admin user...'); + // Create default admin user + const adminUser = await dbService.createUserWithPassword( + adminEmail, + adminPassword, + 'admin' + ); + + console.log('👤 Admin user created:', adminUser); + + // Update user to admin role and active status + const updatedAdmin = { + ...adminUser, + role: UserRole.ADMIN, + status: AccountStatus.ACTIVE, + emailVerified: true, + createdAt: new Date(), + lastLoginAt: new Date(), + }; + + await dbService.updateUser(updatedAdmin); + + console.log('✅ Default admin user created successfully'); + console.log('👤 Final admin user:', updatedAdmin); + console.log('📧 Email:', adminEmail); + console.log('🔑 Password:', adminPassword); + console.log('⚠️ Please change the default password after first login!'); + } catch (error) { + console.error('❌ Failed to create default admin user:', error); + throw error; + } + } + + async seedDatabase(): Promise { + // Prevent multiple seeding attempts + if (DatabaseSeeder.seedingInProgress || DatabaseSeeder.seedingCompleted) { + console.log('🔄 Seeding already in progress or completed, skipping...'); + return; + } + + DatabaseSeeder.seedingInProgress = true; + console.log('🌱 Starting database seeding...'); + + try { + await this.seedDefaultAdmin(); + DatabaseSeeder.seedingCompleted = true; + console.log('🎉 Database seeding completed successfully!'); + } catch (error) { + console.error('💥 Database seeding failed:', error); + throw error; + } finally { + DatabaseSeeder.seedingInProgress = false; + } + } +} + +export const databaseSeeder = new DatabaseSeeder(); + +// The seeding will be called explicitly from App.tsx diff --git a/services/email.ts b/services/email.ts new file mode 100644 index 0000000..35b6446 --- /dev/null +++ b/services/email.ts @@ -0,0 +1,26 @@ +/** + * Mock email service for sending verification emails + */ +export class EmailService { + /** + * Simulates sending a verification email with a link to /verify-email?token=${token} + * @param email - The recipient's email address + * @param token - The verification token + */ + async sendVerificationEmail(email: string, token: string): Promise { + // In a real implementation, this would send an actual email + // For this demo, we'll just log the action + console.log( + `📧 Sending verification email to ${email} with token: ${token}` + ); + console.log(`🔗 Verification link: /verify-email?token=${token}`); + + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 500)); + + // In a real implementation, we would make an HTTP request to an email service + // Example: await fetch('/api/send-email', { method: 'POST', body: JSON.stringify({ email, token }) }); + } +} + +export const emailService = new EmailService(); diff --git a/services/mailgun.config.ts b/services/mailgun.config.ts new file mode 100644 index 0000000..cf64db3 --- /dev/null +++ b/services/mailgun.config.ts @@ -0,0 +1,62 @@ +// Mailgun Configuration +// This file handles Mailgun credentials and configuration + +export interface MailgunConfig { + apiKey: string; + domain: string; + baseUrl: string; + fromName: string; + fromEmail: string; +} + +// Default configuration for development +const defaultConfig: MailgunConfig = { + apiKey: 'demo-key', + domain: 'demo.mailgun.org', + baseUrl: 'https://api.mailgun.net/v3', + fromName: 'Medication Reminder', + fromEmail: 'noreply@demo.mailgun.org', +}; + +// Load configuration from environment variables or use defaults +export const getMailgunConfig = (): MailgunConfig => { + // Check if running in browser environment + const isClient = typeof window !== 'undefined'; + + if (isClient) { + // In browser, use Vite environment variables + // Note: Vite environment variables are available at build time + const env = (import.meta as any).env || {}; + return { + apiKey: env.VITE_MAILGUN_API_KEY || defaultConfig.apiKey, + domain: env.VITE_MAILGUN_DOMAIN || defaultConfig.domain, + baseUrl: env.VITE_MAILGUN_BASE_URL || defaultConfig.baseUrl, + fromName: env.VITE_MAILGUN_FROM_NAME || defaultConfig.fromName, + fromEmail: env.VITE_MAILGUN_FROM_EMAIL || defaultConfig.fromEmail, + }; + } else { + // In Node.js environment (if needed for SSR) + return { + apiKey: process.env.MAILGUN_API_KEY || defaultConfig.apiKey, + domain: process.env.MAILGUN_DOMAIN || defaultConfig.domain, + baseUrl: process.env.MAILGUN_BASE_URL || defaultConfig.baseUrl, + fromName: process.env.MAILGUN_FROM_NAME || defaultConfig.fromName, + fromEmail: process.env.MAILGUN_FROM_EMAIL || defaultConfig.fromEmail, + }; + } +}; + +// Check if Mailgun is properly configured (not using demo values) +export const isMailgunConfigured = (): boolean => { + const config = getMailgunConfig(); + return ( + config.apiKey !== 'demo-key' && + config.domain !== 'demo.mailgun.org' && + config.apiKey.length > 0 + ); +}; + +// Development mode check +export const isDevelopmentMode = (): boolean => { + return !isMailgunConfigured(); +}; diff --git a/services/mailgun.service.ts b/services/mailgun.service.ts new file mode 100644 index 0000000..59a0682 --- /dev/null +++ b/services/mailgun.service.ts @@ -0,0 +1,191 @@ +// Mailgun Email Service +// This service handles email sending via Mailgun API + +import { + getMailgunConfig, + isMailgunConfigured, + isDevelopmentMode, + type MailgunConfig, +} from './mailgun.config'; + +interface EmailTemplate { + subject: string; + html: string; + text?: string; +} + +export class MailgunService { + private config: MailgunConfig; + + constructor() { + this.config = getMailgunConfig(); + + // Log configuration status on startup + const status = this.getConfigurationStatus(); + if (status.mode === 'development') { + console.log( + '📧 Mailgun Service: Running in development mode (emails will be logged only)' + ); + console.log( + '💡 To enable real emails, configure Mailgun credentials in .env.local' + ); + } else { + console.log( + '📧 Mailgun Service: Configured for production with domain:', + status.domain + ); + } + } + + private getVerificationEmailTemplate(verificationUrl: string): EmailTemplate { + return { + subject: 'Verify Your Email - Medication Reminder', + html: ` +
    +

    Verify Your Email Address

    +

    Thank you for signing up for Medication Reminder! Please click the button below to verify your email address:

    + +

    Or copy and paste this link into your browser:

    +

    ${verificationUrl}

    +

    This link will expire in 24 hours.

    +
    + `, + text: ` + Verify Your Email - Medication Reminder + + Thank you for signing up! Please verify your email by visiting: + ${verificationUrl} + + This link will expire in 24 hours. + `, + }; + } + + private getPasswordResetEmailTemplate(resetUrl: string): EmailTemplate { + return { + subject: 'Reset Your Password - Medication Reminder', + html: ` +
    +

    Reset Your Password

    +

    You requested to reset your password. Click the button below to set a new password:

    + +

    Or copy and paste this link into your browser:

    +

    ${resetUrl}

    +

    This link will expire in 1 hour. If you didn't request this, please ignore this email.

    +
    + `, + text: ` + Reset Your Password - Medication Reminder + + You requested to reset your password. Visit this link to set a new password: + ${resetUrl} + + This link will expire in 1 hour. If you didn't request this, please ignore this email. + `, + }; + } + + async sendEmail(to: string, template: EmailTemplate): Promise { + try { + // In development mode or when Mailgun is not configured, just log the email + if (isDevelopmentMode()) { + console.log('📧 Mock Email Sent (Development Mode):', { + to, + subject: template.subject, + from: `${this.config.fromName} <${this.config.fromEmail}>`, + html: template.html, + text: template.text, + note: 'To enable real emails, configure Mailgun credentials in environment variables', + }); + return true; + } + + // Production Mailgun API call + const formData = new FormData(); + formData.append( + 'from', + `${this.config.fromName} <${this.config.fromEmail}>` + ); + formData.append('to', to); + formData.append('subject', template.subject); + formData.append('html', template.html); + if (template.text) { + formData.append('text', template.text); + } + + const response = await fetch( + `${this.config.baseUrl}/${this.config.domain}/messages`, + { + method: 'POST', + headers: { + Authorization: `Basic ${btoa(`api:${this.config.apiKey}`)}`, + }, + body: formData, + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Mailgun API error: ${response.status} - ${errorText}`); + } + + const result = await response.json(); + console.log('📧 Email sent successfully via Mailgun:', { + to, + subject: template.subject, + messageId: result.id, + }); + + return true; + } catch (error) { + console.error('Email sending failed:', error); + return false; + } + } + + async sendVerificationEmail(email: string, token: string): Promise { + const baseUrl = process.env.APP_BASE_URL || 'http://localhost:5173'; + const verificationUrl = `${baseUrl}/verify-email?token=${token}`; + const template = this.getVerificationEmailTemplate(verificationUrl); + return this.sendEmail(email, template); + } + + async sendPasswordResetEmail(email: string, token: string): Promise { + const baseUrl = process.env.APP_BASE_URL || 'http://localhost:5173'; + const resetUrl = `${baseUrl}/reset-password?token=${token}`; + const template = this.getPasswordResetEmailTemplate(resetUrl); + return this.sendEmail(email, template); + } + + // Utility method to check if Mailgun is properly configured + isConfigured(): boolean { + return isMailgunConfigured(); + } + + // Get configuration status for debugging + getConfigurationStatus(): { + configured: boolean; + mode: 'development' | 'production'; + domain: string; + fromEmail: string; + } { + return { + configured: isMailgunConfigured(), + mode: isDevelopmentMode() ? 'development' : 'production', + domain: this.config.domain, + fromEmail: this.config.fromEmail, + }; + } +} + +export const mailgunService = new MailgunService(); diff --git a/services/oauth.ts b/services/oauth.ts new file mode 100644 index 0000000..770ed47 --- /dev/null +++ b/services/oauth.ts @@ -0,0 +1,139 @@ +import { authService } from './auth/auth.service'; +import { OAuthProvider, OAuthState, User } from '../types'; +import { dbService } from './couchdb.factory'; +import { AccountStatus } from './auth/auth.constants'; + +// Mock OAuth configuration +const GOOGLE_CLIENT_ID = 'mock_google_client_id'; +const GITHUB_CLIENT_ID = 'mock_github_client_id'; + +// Mock OAuth endpoints +const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; +const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; + +// Mock token exchange endpoints +const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'; +const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; + +// Mock OAuth scopes +const GOOGLE_SCOPES = 'openid email profile'; +const GITHUB_SCOPES = 'user:email'; + +// Mock redirect URI +const REDIRECT_URI = 'http://localhost:3000/auth/callback'; + +// Mock OAuth state generation +const generateState = () => crypto.randomUUID(); + +export const googleAuth = () => { + const state = generateState(); + const url = new URL(GOOGLE_AUTH_URL); + url.searchParams.append('client_id', GOOGLE_CLIENT_ID); + url.searchParams.append('response_type', 'code'); + url.searchParams.append('scope', GOOGLE_SCOPES); + url.searchParams.append('redirect_uri', REDIRECT_URI); + url.searchParams.append('state', state); + + // In a real implementation, we would store the state in the session or localStorage + localStorage.setItem('oauth_state', state); + + // Redirect to Google's auth endpoint + window.location.href = url.toString(); +}; + +export const githubAuth = () => { + const state = generateState(); + const url = new URL(GITHUB_AUTH_URL); + url.searchParams.append('client_id', GITHUB_CLIENT_ID); + url.searchParams.append('response_type', 'code'); + url.searchParams.append('scope', GITHUB_SCOPES); + url.searchParams.append('redirect_uri', REDIRECT_URI); + url.searchParams.append('state', state); + + // In a real implementation, we would store the state in the session or localStorage + localStorage.setItem('oauth_state', state); + + // Redirect to GitHub's auth endpoint + window.location.href = url.toString(); +}; + +// Mock token exchange +const mockExchangeCodeForToken = async ( + provider: 'google' | 'github', + code: string +): Promise => { + // In a real implementation, we would make a POST request to the token endpoint + // with the code, client_id, client_secret, and redirect_uri + + // For this mock, we'll just return a mock access token + return `mock_${provider}_access_token_${crypto.randomUUID()}`; +}; + +// Mock user info retrieval +const mockGetUserInfo = async ( + provider: 'google' | 'github', + accessToken: string +): Promise<{ email: string; name: string }> => { + // In a real implementation, we would make a GET request to the user info endpoint + // with the access token + + // For this mock, we'll return mock user info + return { + email: `mock_${provider}_user_${crypto.randomUUID()}@example.com`, + name: `Mock ${provider.charAt(0).toUpperCase() + provider.slice(1)} User`, + }; +}; + +export const handleGoogleCallback = async () => { + const params = new URLSearchParams(window.location.search); + const code = params.get('code'); + const state = params.get('state'); + const storedState = localStorage.getItem('oauth_state'); + + // Verify state to prevent CSRF attacks + if (state !== storedState) { + throw new Error('Invalid OAuth state'); + } + + // Clear stored state + localStorage.removeItem('oauth_state'); + + // Exchange code for token + const accessToken = await mockExchangeCodeForToken('google', code); + + // Get user info + const userInfo = await mockGetUserInfo('google', accessToken); + + // Register or login the user + return authService.loginWithOAuth('google', { + email: userInfo.email, + username: userInfo.name, + }); +}; + +export const handleGithubCallback = async () => { + const params = new URLSearchParams(window.location.search); + const code = params.get('code'); + const state = params.get('state'); + const storedState = localStorage.getItem('oauth_state'); + + // Verify state to prevent CSRF attacks + if (state !== storedState) { + throw new Error('Invalid OAuth state'); + } + + // Clear stored state + localStorage.removeItem('oauth_state'); + + // Exchange code for token + const accessToken = await mockExchangeCodeForToken('github', code); + + // Get user info + const userInfo = await mockGetUserInfo('github', accessToken); + + // Register or login the user + return authService.loginWithOAuth('github', { + email: userInfo.email, + username: userInfo.name, + }); +}; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..c6537d6 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,171 @@ +# 🧪 Testing Documentation + +## Test Structure + +``` +tests/ +├── setup.ts # Jest configuration and global test setup +├── integration/ # Integration tests for production validation +│ └── production.test.js # CouchDB, deployment, and service testing +├── manual/ # Manual testing scripts and debugging tools +│ ├── admin-login-debug.js # Browser console debugging for admin login +│ ├── auth-db-debug.js # Authentication database debugging +│ └── debug-email-validation.js # Email validation debugging +└── e2e/ # End-to-end tests with Playwright + ├── README.md # E2E testing documentation + ├── fixtures.ts # Custom test fixtures + ├── helpers.ts # Test utilities and data + ├── auth.spec.ts # Authentication flow tests + ├── medication.spec.ts # Medication management tests + ├── admin.spec.ts # Admin interface tests + ├── ui-navigation.spec.ts # UI and navigation tests + └── reminders.spec.ts # Reminder system tests + +services/ +└── auth/ + └── __tests__/ # Unit tests for authentication services + ├── auth.integration.test.ts + └── emailVerification.test.ts +``` + +## Running Tests + +### Unit Tests (Jest) + +```bash +# Run all unit tests +bun run test + +# Run tests in watch mode +bun run test:watch + +# Run with coverage +bun run test:coverage + +# Run specific test file +bun run test auth.integration.test.ts +``` + +### Integration Tests + +```bash +# Run production integration tests +bun run test:integration + +# Run all tests (unit + integration + e2e) +bun run test:all + +# Run E2E tests with Playwright +bun run test:e2e + +# Run E2E tests in UI mode +bun run test:e2e:ui + +# Debug E2E tests +bun run test:e2e:debug + +# View E2E test reports +bun run test:e2e:report +``` + +### Manual Testing Scripts + +#### Admin Login Debug + +```bash +# Open browser to http://localhost:8080 +# Open developer console +# Run: +bun tests/manual/admin-login-debug.js +``` + +#### Auth Database Debug + +```bash +# Open browser to http://localhost:8080 +# Open developer console +# Run: +bun tests/manual/auth-db-debug.js +``` + +## Test Categories + +### ✅ Unit Tests (`services/auth/__tests__/`) + +- **Purpose**: Test individual functions and services in isolation +- **Framework**: Jest + TypeScript +- **Coverage**: Authentication, email verification +- **Status**: ✅ Well-structured and maintained + +### 🔧 Integration Tests (`tests/integration/`) + +- **Purpose**: Test entire system interactions and deployment validation +- **Framework**: Bun native testing +- **Coverage**: CouchDB connectivity, database setup, production readiness +- **Status**: ✅ Useful for deployment validation + +### 🛠️ Manual Tests (`tests/manual/`) + +- **Purpose**: Browser-based debugging and manual verification +- **Framework**: Vanilla JavaScript for browser console +- **Coverage**: Admin authentication, database debugging +- **Status**: ⚠️ Useful for debugging but should be automated + +### 🎯 E2E Tests (`tests/e2e/`) + +- **Purpose**: Full user journey testing across browsers +- **Framework**: Playwright with TypeScript +- **Coverage**: Complete user workflows, cross-browser compatibility +- **Status**: ✅ Comprehensive test suite with 5 spec files + +## Test Configuration + +### Jest Configuration (`jest.config.json`) + +- TypeScript support with ts-jest +- jsdom environment for DOM testing +- Coverage reporting +- Module path mapping + +### Test Setup (`tests/setup.ts`) + +- localStorage mocking +- fetch mocking +- Console noise reduction +- Global test utilities + +## Recommendations + +### ✅ Keep These Tests + +1. **Authentication unit tests** - Critical for security +2. **Production integration tests** - Essential for deployment validation +3. **Manual debugging scripts** - Useful for development + +### 🔄 Future Improvements + +1. **Remove temporary type declarations** once Playwright types are fully recognized +2. **Increase unit test coverage** for components and utilities +3. **Add visual regression tests** using Playwright screenshots +4. **Implement accessibility testing** in E2E suite +5. **Add performance tests** for large datasets +6. **Set up test data management** for E2E tests + +### 🛡️ Testing Best Practices + +- Run tests before every deployment +- Maintain >80% code coverage for critical paths +- Use integration tests to validate environment setup +- Keep manual tests for complex debugging scenarios + +## CI/CD Integration + +Add to your deployment pipeline: + +```bash +# Validate tests before deployment +bun run test:all + +# Run in production validation +bun run test:integration +``` diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..fd48941 --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,319 @@ +# 🎭 End-to-End Testing with Playwright + +## Overview + +This directory contains comprehensive end-to-end tests for the Medication Reminder App using Playwright. These tests simulate real user interactions across different browsers and devices. + +## Test Structure + +``` +tests/e2e/ +├── README.md # This documentation +├── fixtures.ts # Custom test fixtures and utilities +├── helpers.ts # Test helper functions and data +├── auth.spec.ts # Authentication flow tests +├── medication.spec.ts # Medication management tests +├── admin.spec.ts # Admin interface tests +├── ui-navigation.spec.ts # UI and navigation tests +└── reminders.spec.ts # Reminder system tests +``` + +## Test Categories + +### 🔐 **Authentication Tests** (`auth.spec.ts`) + +- User registration and login +- Admin authentication +- OAuth button visibility +- Invalid credential handling +- Session management + +### 💊 **Medication Management** (`medication.spec.ts`) + +- Adding new medications +- Editing existing medications +- Deleting medications +- Marking doses as taken +- Viewing medication history + +### 👑 **Admin Interface** (`admin.spec.ts`) + +- User management operations +- Password changes +- User status toggles +- Admin permissions + +### 🎨 **UI & Navigation** (`ui-navigation.spec.ts`) + +- Theme switching +- Modal interactions +- Responsive design +- Search functionality +- Account management + +### ⏰ **Reminder System** (`reminders.spec.ts`) + +- Custom reminder creation +- Reminder editing and deletion +- Scheduled dose display +- Missed dose handling + +## Setup and Installation + +### 1. Install Playwright + +```bash +# Install Playwright and browsers +npm install -D @playwright/test +npx playwright install +``` + +### 2. Update Package.json + +```json +{ + "scripts": { + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", + "test:e2e:report": "playwright show-report" + } +} +``` + +## Running Tests + +### Basic Test Execution + +```bash +# Run all E2E tests +npm run test:e2e + +# Run tests in UI mode (interactive) +npm run test:e2e:ui + +# Run specific test file +npx playwright test auth.spec.ts + +# Run tests in debug mode +npm run test:e2e:debug +``` + +### Browser-Specific Testing + +```bash +# Run on specific browser +npx playwright test --project=chromium +npx playwright test --project=firefox +npx playwright test --project=webkit + +# Run on mobile browsers +npx playwright test --project="Mobile Chrome" +npx playwright test --project="Mobile Safari" +``` + +### Test Reporting + +```bash +# Generate and view HTML report +npm run test:e2e:report + +# Run with specific reporter +npx playwright test --reporter=line +npx playwright test --reporter=json +``` + +## Test Configuration + +The tests are configured via `playwright.config.ts`: + +- **Base URL**: `http://localhost:8080` +- **Auto-start**: Docker Compose before tests +- **Browsers**: Chrome, Firefox, Safari, Mobile Chrome, Mobile Safari +- **Retries**: 2 on CI, 0 locally +- **Screenshots**: On failure +- **Videos**: On failure +- **Traces**: On retry + +## Test Data and Fixtures + +### Custom Fixtures (`fixtures.ts`) + +- `adminPage`: Auto-login as admin user +- `userPage`: Auto-login as regular user + +### Helper Functions (`helpers.ts`) + +- `MedicationHelpers`: Medication CRUD operations +- `AuthHelpers`: Authentication actions +- `ModalHelpers`: Modal interactions +- `WaitHelpers`: Wait utilities +- `TestData`: Pre-defined test data + +### Example Usage + +```typescript +import { test } from './fixtures'; +import { MedicationHelpers, TestData } from './helpers'; + +test('should add medication', async ({ adminPage }) => { + const medicationHelper = new MedicationHelpers(adminPage); + const testMed = TestData.medications[0]; + + await medicationHelper.addMedication(testMed.name, testMed.dosage, testMed.frequency); +}); +``` + +## Best Practices + +### ✅ Test Organization + +- Group related tests in describe blocks +- Use descriptive test names +- Keep tests independent and isolated + +### ✅ Selectors + +- Use data-testid attributes for reliable targeting +- Prefer semantic selectors (role, text content) +- Avoid CSS selectors that may change + +### ✅ Waiting Strategies + +- Use `waitForSelector()` for dynamic content +- Leverage auto-waiting for most actions +- Add explicit waits for complex interactions + +### ✅ Test Data + +- Use helper functions for common operations +- Keep test data in centralized location +- Clean up test data after tests + +## Debugging Tests + +### Local Debugging + +```bash +# Debug specific test +npx playwright test auth.spec.ts --debug + +# Run with headed browser +npx playwright test --headed + +# Slow down execution +npx playwright test --slow-mo=1000 +``` + +### CI/CD Integration + +```bash +# Run in CI mode +CI=true npx playwright test + +# Generate artifacts for CI +npx playwright test --reporter=github +``` + +## Adding New Tests + +### 1. Create Test File + +```typescript +import { test, expect } from './fixtures'; + +test.describe('New Feature', () => { + test('should do something', async ({ adminPage }) => { + // Test implementation + }); +}); +``` + +### 2. Add Helper Functions + +```typescript +// In helpers.ts +export class NewFeatureHelpers { + constructor(private page: any) {} + + async performAction() { + // Helper implementation + } +} +``` + +### 3. Update Documentation + +- Add test description to this README +- Update test count in project documentation + +## Troubleshooting + +### Common Issues + +**Tests timeout:** + +- Increase timeout in config +- Add explicit waits +- Check application startup time + +**Flaky tests:** + +- Add proper wait conditions +- Use retry logic +- Check for race conditions + +**Browser compatibility:** + +- Test across all configured browsers +- Check for browser-specific issues +- Use cross-browser compatible selectors + +### Debug Commands + +```bash +# Show browser developer tools +npx playwright test --debug + +# Record test execution +npx playwright codegen localhost:8080 + +# Trace viewer +npx playwright show-trace trace.zip +``` + +## Continuous Integration + +Example GitHub Actions workflow: + +```yaml +name: E2E Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: npm ci + - run: npx playwright install --with-deps + - run: npm run test:e2e + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ +``` + +## Coverage and Metrics + +The E2E tests provide coverage for: + +- ✅ User authentication flows +- ✅ Core medication management +- ✅ Admin functionality +- ✅ UI interactions and navigation +- ✅ Responsive design +- ✅ Cross-browser compatibility + +For optimal coverage, run tests regularly and add new tests for new features. diff --git a/tests/e2e/admin.spec.ts b/tests/e2e/admin.spec.ts new file mode 100644 index 0000000..51542a6 --- /dev/null +++ b/tests/e2e/admin.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Admin Interface', () => { + test.beforeEach(async ({ page }) => { + // Login as admin + await page.goto('/'); + await page.fill('input[type="email"]', 'admin@localhost'); + await page.fill('input[type="password"]', 'admin123!'); + await page.click('button[type="submit"]'); + + // Wait for main app and open admin interface + await expect(page.locator('h1')).toContainText('Medication Reminder'); + await page.click('button:has-text("Admin")'); + }); + + test('should display admin interface', async ({ page }) => { + await expect(page.locator('text=Admin Interface')).toBeVisible(); + await expect(page.locator('text=User Management')).toBeVisible(); + }); + + test('should show list of users', async ({ page }) => { + // Should show admin user at minimum + await expect(page.locator('text=admin@localhost')).toBeVisible(); + await expect(page.locator('text=Admin')).toBeVisible(); // Role + }); + + test('should allow changing user password', async ({ page }) => { + // Click on a user's change password button + await page.click('[data-testid="change-password"]'); + + // Fill new password + await page.fill('input[type="password"]', 'NewPassword123!'); + + // Submit password change + await page.click('button:has-text("Change Password")'); + + // Should show success message + await expect(page.locator('text=Password changed')).toBeVisible(); + }); + + test('should allow suspending/activating users', async ({ page }) => { + // Look for user status controls + const statusButton = page + .locator('[data-testid="toggle-user-status"]') + .first(); + await expect(statusButton).toBeVisible(); + }); + + test('should refresh user list', async ({ page }) => { + await page.click('button:has-text("Refresh")'); + + // Should still show users after refresh + await expect(page.locator('text=admin@localhost')).toBeVisible(); + }); + + test('should close admin interface', async ({ page }) => { + await page.click('button[aria-label="Close"]'); + + // Should return to main app + await expect(page.locator('text=Admin Interface')).not.toBeVisible(); + await expect(page.locator('h1')).toContainText('Medication Reminder'); + }); +}); diff --git a/tests/e2e/auth.spec.ts b/tests/e2e/auth.spec.ts new file mode 100644 index 0000000..05a9383 --- /dev/null +++ b/tests/e2e/auth.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Authentication Flow', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display login page for unauthenticated users', async ({ + page, + }) => { + await expect(page.locator('h2')).toContainText(['Sign In', 'Login']); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + }); + + test('should allow user registration', async ({ page }) => { + // Click register tab/link + await page.click('text=Register'); + + // Fill registration form + await page.fill('input[type="email"]', 'test@example.com'); + await page.fill('input[name="username"]', 'testuser'); + await page.fill('input[type="password"]', 'TestPassword123!'); + + // Submit registration + await page.click('button[type="submit"]'); + + // Should show verification message or redirect + await expect(page.locator('text=verification')).toBeVisible(); + }); + + test('should login with admin credentials', async ({ page }) => { + // Fill login form with admin credentials + await page.fill('input[type="email"]', 'admin@localhost'); + await page.fill('input[type="password"]', 'admin123!'); + + // Submit login + await page.click('button[type="submit"]'); + + // Should redirect to main app + await expect(page.locator('h1')).toContainText('Medication Reminder'); + await expect(page.locator('text=Admin')).toBeVisible(); + }); + + test('should show error for invalid credentials', async ({ page }) => { + await page.fill('input[type="email"]', 'invalid@example.com'); + await page.fill('input[type="password"]', 'wrongpassword'); + + await page.click('button[type="submit"]'); + + await expect(page.locator('text=Invalid')).toBeVisible(); + }); + + test('should handle OAuth login buttons', async ({ page }) => { + await expect(page.locator('button:has-text("Google")')).toBeVisible(); + await expect(page.locator('button:has-text("GitHub")')).toBeVisible(); + }); +}); diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts new file mode 100644 index 0000000..505d81b --- /dev/null +++ b/tests/e2e/fixtures.ts @@ -0,0 +1,48 @@ +import { test as base } from '@playwright/test'; + +// Extend basic test with custom fixtures +export const test = base.extend({ + // Auto-login fixture for admin user + adminPage: async ({ page }, use) => { + await page.goto('/'); + await page.fill('input[type="email"]', 'admin@localhost'); + await page.fill('input[type="password"]', 'admin123!'); + await page.click('button[type="submit"]'); + + // Wait for app to load + await page.waitForSelector('h1:has-text("Medication Reminder")'); + + await use(page); + }, + + // Regular user login fixture + userPage: async ({ page }, use) => { + await page.goto('/'); + + // Register a test user first if needed + await page.click('text=Register'); + await page.fill('input[type="email"]', 'testuser@example.com'); + await page.fill('input[name="username"]', 'testuser'); + await page.fill('input[type="password"]', 'TestPassword123!'); + await page.click('button[type="submit"]'); + + // For mock database, user might be auto-verified + // Wait for either verification message or app load + try { + await page.waitForSelector('h1:has-text("Medication Reminder")', { + timeout: 5000, + }); + } catch { + // If not auto-logged in, login manually + await page.goto('/'); + await page.fill('input[type="email"]', 'testuser@example.com'); + await page.fill('input[type="password"]', 'TestPassword123!'); + await page.click('button[type="submit"]'); + await page.waitForSelector('h1:has-text("Medication Reminder")'); + } + + await use(page); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts new file mode 100644 index 0000000..40be687 --- /dev/null +++ b/tests/e2e/helpers.ts @@ -0,0 +1,131 @@ +// E2E Test Utilities and Helpers + +export class MedicationHelpers { + constructor(private page: any) {} + + async addMedication( + name: string, + dosage: string, + frequency: string = 'daily', + times: string = '1' + ) { + await this.page.click('button:has-text("Add Medication")'); + await this.page.fill('input[name="name"]', name); + await this.page.fill('input[name="dosage"]', dosage); + await this.page.selectOption('select[name="frequency"]', frequency); + if (times !== '1') { + await this.page.fill('input[name="times"]', times); + } + await this.page.click('button[type="submit"]'); + + // Wait for medication to appear + await this.page.waitForSelector(`text=${name}`); + } + + async deleteMedication(name: string) { + await this.page.click('button:has-text("Manage")'); + + // Find the medication row and click delete + const medicationRow = this.page.locator(`tr:has-text("${name}")`); + await medicationRow.locator('[data-testid="delete-medication"]').click(); + await this.page.click('button:has-text("Delete")'); + + // Close manage modal + await this.page.click('button:has-text("Close")'); + } + + async takeDose(medicationName: string) { + const doseCard = this.page.locator( + `.dose-card:has-text("${medicationName}")` + ); + await doseCard.locator('button:has-text("Take")').click(); + } +} + +export class AuthHelpers { + constructor(private page: any) {} + + async loginAsAdmin() { + await this.page.goto('/'); + await this.page.fill('input[type="email"]', 'admin@localhost'); + await this.page.fill('input[type="password"]', 'admin123!'); + await this.page.click('button[type="submit"]'); + await this.page.waitForSelector('h1:has-text("Medication Reminder")'); + } + + async registerUser(email: string, username: string, password: string) { + await this.page.goto('/'); + await this.page.click('text=Register'); + await this.page.fill('input[type="email"]', email); + await this.page.fill('input[name="username"]', username); + await this.page.fill('input[type="password"]', password); + await this.page.click('button[type="submit"]'); + } + + async logout() { + await this.page.click('[data-testid="avatar-dropdown"]'); + await this.page.click('button:has-text("Logout")'); + await this.page.waitForSelector('h2:has-text("Sign In")'); + } +} + +export class ModalHelpers { + constructor(private page: any) {} + + async openModal(buttonText: string) { + await this.page.click(`button:has-text("${buttonText}")`); + } + + async closeModal() { + await this.page.click('button:has-text("Close")'); + } + + async confirmAction() { + await this.page.click('button:has-text("Confirm")'); + } +} + +export class WaitHelpers { + constructor(private page: any) {} + + async waitForAppLoad() { + await this.page.waitForSelector('h1:has-text("Medication Reminder")'); + } + + async waitForModal(title: string) { + await this.page.waitForSelector(`text=${title}`); + } + + async waitForNotification(message: string) { + await this.page.waitForSelector(`text=${message}`); + } +} + +// Data generators for testing +export const TestData = { + medications: [ + { name: 'Aspirin', dosage: '100mg', frequency: 'daily', times: '1' }, + { name: 'Vitamin D', dosage: '1000 IU', frequency: 'daily', times: '1' }, + { name: 'Omega-3', dosage: '500mg', frequency: 'daily', times: '2' }, + { name: 'Calcium', dosage: '600mg', frequency: 'twice_daily', times: '1' }, + ], + + users: [ + { + email: 'test1@example.com', + username: 'testuser1', + password: 'TestPass123!', + }, + { + email: 'test2@example.com', + username: 'testuser2', + password: 'TestPass456!', + }, + ], + + reminders: [ + { title: 'Drink Water', icon: 'bell', frequency: 60 }, + { title: 'Exercise', icon: 'heart', frequency: 1440 }, // Daily + { title: 'Check Blood Pressure', icon: 'chart', frequency: 10080 }, // Weekly + ], +}; diff --git a/tests/e2e/medication.spec.ts b/tests/e2e/medication.spec.ts new file mode 100644 index 0000000..33f8d2d --- /dev/null +++ b/tests/e2e/medication.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Medication Management', () => { + test.beforeEach(async ({ page }) => { + // Login as admin first + await page.goto('/'); + await page.fill('input[type="email"]', 'admin@localhost'); + await page.fill('input[type="password"]', 'admin123!'); + await page.click('button[type="submit"]'); + + // Wait for main app to load + await expect(page.locator('h1')).toContainText('Medication Reminder'); + }); + + test('should add a new medication', async ({ page }) => { + // Click add medication button + await page.click('button:has-text("Add Medication")'); + + // Fill medication form + await page.fill('input[name="name"]', 'Aspirin'); + await page.fill('input[name="dosage"]', '100mg'); + await page.selectOption('select[name="frequency"]', 'daily'); + await page.fill('input[name="times"]', '2'); + + // Submit form + await page.click('button[type="submit"]'); + + // Should see medication in list + await expect(page.locator('text=Aspirin')).toBeVisible(); + await expect(page.locator('text=100mg')).toBeVisible(); + }); + + test('should edit existing medication', async ({ page }) => { + // First add a medication + await page.click('button:has-text("Add Medication")'); + await page.fill('input[name="name"]', 'Vitamin D'); + await page.fill('input[name="dosage"]', '1000 IU'); + await page.click('button[type="submit"]'); + + // Click edit button for the medication + await page.click('[data-testid="edit-medication"]'); + + // Update dosage + await page.fill('input[name="dosage"]', '2000 IU'); + await page.click('button[type="submit"]'); + + // Should see updated dosage + await expect(page.locator('text=2000 IU')).toBeVisible(); + }); + + test('should delete medication', async ({ page }) => { + // Add a medication first + await page.click('button:has-text("Add Medication")'); + await page.fill('input[name="name"]', 'Test Medicine'); + await page.fill('input[name="dosage"]', '50mg'); + await page.click('button[type="submit"]'); + + // Open manage medications modal + await page.click('button:has-text("Manage")'); + + // Delete the medication + await page.click('[data-testid="delete-medication"]'); + await page.click('button:has-text("Delete")'); // Confirm deletion + + // Should not see medication anymore + await expect(page.locator('text=Test Medicine')).not.toBeVisible(); + }); + + test('should mark dose as taken', async ({ page }) => { + // Add a medication first + await page.click('button:has-text("Add Medication")'); + await page.fill('input[name="name"]', 'Daily Vitamin'); + await page.fill('input[name="dosage"]', '1 tablet'); + await page.selectOption('select[name="frequency"]', 'daily'); + await page.click('button[type="submit"]'); + + // Find and click the "Take" button for upcoming dose + await page.click('button:has-text("Take")'); + + // Should show as taken + await expect(page.locator('text=Taken')).toBeVisible(); + await expect(page.locator('.bg-green-50')).toBeVisible(); + }); + + test('should show medication history', async ({ page }) => { + // Open history modal + await page.click('button:has-text("History")'); + + // Should show history modal + await expect(page.locator('text=Medication History')).toBeVisible(); + + // Close modal + await page.click('button:has-text("Close")'); + }); +}); diff --git a/tests/e2e/reminders.spec.ts b/tests/e2e/reminders.spec.ts new file mode 100644 index 0000000..50973b1 --- /dev/null +++ b/tests/e2e/reminders.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Reminder System', () => { + test.beforeEach(async ({ page }) => { + // Login as admin + await page.goto('/'); + await page.fill('input[type="email"]', 'admin@localhost'); + await page.fill('input[type="password"]', 'admin123!'); + await page.click('button[type="submit"]'); + await expect(page.locator('h1')).toContainText('Medication Reminder'); + }); + + test('should create custom reminder', async ({ page }) => { + // Click manage reminders + await page.click('button:has-text("Reminders")'); + + // Add new reminder + await page.click('button:has-text("Add Reminder")'); + + // Fill reminder form + await page.fill('input[name="title"]', 'Drink Water'); + await page.selectOption('select[name="icon"]', 'bell'); + await page.fill('input[name="frequency"]', '60'); // Every hour + + await page.click('button[type="submit"]'); + + // Should see reminder in list + await expect(page.locator('text=Drink Water')).toBeVisible(); + }); + + test('should edit custom reminder', async ({ page }) => { + // First create a reminder + await page.click('button:has-text("Reminders")'); + await page.click('button:has-text("Add Reminder")'); + await page.fill('input[name="title"]', 'Exercise'); + await page.click('button[type="submit"]'); + + // Edit the reminder + await page.click('[data-testid="edit-reminder"]'); + await page.fill('input[name="title"]', 'Morning Exercise'); + await page.click('button[type="submit"]'); + + await expect(page.locator('text=Morning Exercise')).toBeVisible(); + }); + + test('should delete custom reminder', async ({ page }) => { + // Create and then delete reminder + await page.click('button:has-text("Reminders")'); + await page.click('button:has-text("Add Reminder")'); + await page.fill('input[name="title"]', 'Temporary Reminder'); + await page.click('button[type="submit"]'); + + // Delete it + await page.click('[data-testid="delete-reminder"]'); + await page.click('button:has-text("Delete")'); // Confirm + + await expect(page.locator('text=Temporary Reminder')).not.toBeVisible(); + }); + + test('should show scheduled medication doses', async ({ page }) => { + // Add a medication first + await page.click('button:has-text("Add Medication")'); + await page.fill('input[name="name"]', 'Scheduled Med'); + await page.fill('input[name="dosage"]', '5mg'); + await page.selectOption('select[name="frequency"]', 'daily'); + await page.fill('input[name="times"]', '3'); // 3 times daily + await page.click('button[type="submit"]'); + + // Should see scheduled doses for today + await expect(page.locator('text=Scheduled Med')).toBeVisible(); + await expect(page.locator('button:has-text("Take")')).toHaveCount(3); + }); + + test('should handle missed doses', async ({ page }) => { + // This would test the logic for marking doses as missed + // when they pass their scheduled time + + // Add medication with past schedule + await page.click('button:has-text("Add Medication")'); + await page.fill('input[name="name"]', 'Past Due Med'); + await page.fill('input[name="dosage"]', '10mg'); + await page.click('button[type="submit"]'); + + // Simulate time passing or manually mark as missed + // This would depend on your app's specific implementation + }); +}); diff --git a/tests/e2e/ui-navigation.spec.ts b/tests/e2e/ui-navigation.spec.ts new file mode 100644 index 0000000..284efd2 --- /dev/null +++ b/tests/e2e/ui-navigation.spec.ts @@ -0,0 +1,100 @@ +import { test, expect } from '@playwright/test'; + +test.describe('User Interface and Navigation', () => { + test.beforeEach(async ({ page }) => { + // Login as admin + await page.goto('/'); + await page.fill('input[type="email"]', 'admin@localhost'); + await page.fill('input[type="password"]', 'admin123!'); + await page.click('button[type="submit"]'); + await expect(page.locator('h1')).toContainText('Medication Reminder'); + }); + + test('should display main navigation elements', async ({ page }) => { + await expect( + page.locator('button:has-text("Add Medication")') + ).toBeVisible(); + await expect(page.locator('button:has-text("Manage")')).toBeVisible(); + await expect(page.locator('button:has-text("History")')).toBeVisible(); + await expect(page.locator('button:has-text("Stats")')).toBeVisible(); + }); + + test('should toggle theme', async ({ page }) => { + // Click theme switcher + await page.click('[data-testid="theme-switcher"]'); + + // Check if dark mode is applied + await expect(page.locator('html')).toHaveClass(/dark/); + + // Toggle back to light mode + await page.click('[data-testid="theme-switcher"]'); + await expect(page.locator('html')).not.toHaveClass(/dark/); + }); + + test('should open and close account modal', async ({ page }) => { + // Click account button + await page.click('button:has-text("Account")'); + + // Should show account modal + await expect(page.locator('text=Account Settings')).toBeVisible(); + + // Close modal + await page.click('button:has-text("Close")'); + await expect(page.locator('text=Account Settings')).not.toBeVisible(); + }); + + test('should open stats modal', async ({ page }) => { + await page.click('button:has-text("Stats")'); + + await expect(page.locator('text=Medication Statistics')).toBeVisible(); + await expect(page.locator('text=Weekly Adherence')).toBeVisible(); + + await page.click('button:has-text("Close")'); + }); + + test('should show current time and date', async ({ page }) => { + // Should display current time somewhere on the page + const timeElement = page.locator('[data-testid="current-time"]'); + await expect(timeElement).toBeVisible(); + }); + + test('should handle responsive design', async ({ page }) => { + // Test mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + + // Should still show main elements + await expect(page.locator('h1')).toBeVisible(); + await expect( + page.locator('button:has-text("Add Medication")') + ).toBeVisible(); + + // Reset to desktop + await page.setViewportSize({ width: 1280, height: 720 }); + }); + + test('should search medications', async ({ page }) => { + // Add a test medication first + await page.click('button:has-text("Add Medication")'); + await page.fill('input[name="name"]', 'Searchable Medicine'); + await page.fill('input[name="dosage"]', '10mg'); + await page.click('button[type="submit"]'); + + // Use search functionality + await page.fill('input[placeholder*="search"]', 'Searchable'); + + // Should show filtered results + await expect(page.locator('text=Searchable Medicine')).toBeVisible(); + + // Clear search + await page.fill('input[placeholder*="search"]', ''); + }); + + test('should logout user', async ({ page }) => { + // Click logout (usually in avatar dropdown) + await page.click('[data-testid="avatar-dropdown"]'); + await page.click('button:has-text("Logout")'); + + // Should return to login page + await expect(page.locator('h2')).toContainText(['Sign In', 'Login']); + }); +}); diff --git a/tests/integration/production.test.js b/tests/integration/production.test.js new file mode 100644 index 0000000..333b425 --- /dev/null +++ b/tests/integration/production.test.js @@ -0,0 +1,78 @@ +#!/usr/bin/env bun + +// Production environment test script +console.log('🧪 Testing Production Environment...\n'); + +// Test 1: Check if CouchDB is accessible +console.log('1️⃣ Testing CouchDB connection...'); +try { + const response = await fetch('http://localhost:5984/', { + headers: { + Authorization: 'Basic ' + btoa('admin:password'), + }, + }); + + if (response.ok) { + const data = await response.json(); + console.log('✅ CouchDB is accessible'); + console.log(` Version: ${data.version}`); + } else { + console.log('❌ CouchDB connection failed'); + } +} catch (error) { + console.log('❌ CouchDB connection error:', error.message); +} + +// Test 2: Check if databases exist +console.log('\n2️⃣ Checking databases...'); +try { + const response = await fetch('http://localhost:5984/_all_dbs', { + headers: { + Authorization: 'Basic ' + btoa('admin:password'), + }, + }); + + if (response.ok) { + const databases = await response.json(); + console.log('✅ Available databases:', databases); + + const requiredDbs = [ + 'users', + 'medications', + 'settings', + 'taken_doses', + 'reminders', + ]; + const missing = requiredDbs.filter(db => !databases.includes(db)); + + if (missing.length === 0) { + console.log('✅ All required databases exist'); + } else { + console.log('⚠️ Missing databases:', missing); + } + } +} catch (error) { + console.log('❌ Database check error:', error.message); +} + +// Test 3: Check if frontend is accessible +console.log('\n3️⃣ Testing frontend accessibility...'); +try { + const response = await fetch('http://localhost:8080/'); + + if (response.ok) { + console.log('✅ Frontend is accessible at http://localhost:8080'); + } else { + console.log('❌ Frontend connection failed'); + } +} catch (error) { + console.log('❌ Frontend connection error:', error.message); +} + +console.log('\n🎯 Production Environment Test Summary:'); +console.log(' • CouchDB: http://localhost:5984'); +console.log(' • Frontend: http://localhost:8080'); +console.log(' • Admin Login: admin@localhost / admin123!'); +console.log( + '\n🚀 Your medication reminder app is ready for production testing!' +); diff --git a/tests/manual/admin-login-debug.js b/tests/manual/admin-login-debug.js new file mode 100644 index 0000000..ed83e99 --- /dev/null +++ b/tests/manual/admin-login-debug.js @@ -0,0 +1,34 @@ +// Simple test script to verify admin login functionality +// Run this in the browser console to test admin credentials + +async function testAdminLogin() { + console.log('🧪 Testing admin login...'); + + // Import the services (this won't work directly, but helps us understand the flow) + console.log('Admin credentials:'); + console.log('Email: admin@localhost'); + console.log('Password: admin123!'); + + // Check if admin user exists in localStorage + const users = JSON.parse(localStorage.getItem('users') || '[]'); + console.log('All users in localStorage:', users); + + const adminUser = users.find(u => u.email === 'admin@localhost'); + console.log('Admin user found:', adminUser); + + if (adminUser) { + console.log('Admin user details:'); + console.log('- Email:', adminUser.email); + console.log('- Password:', adminUser.password); + console.log('- Role:', adminUser.role); + console.log('- Status:', adminUser.status); + console.log('- Email Verified:', adminUser.emailVerified); + } else { + console.log('❌ Admin user not found in localStorage'); + } +} + +// Instructions +console.log('Copy and paste this function in browser console:'); +console.log(testAdminLogin.toString()); +console.log('Then run: testAdminLogin()'); diff --git a/tests/manual/auth-db-debug.js b/tests/manual/auth-db-debug.js new file mode 100644 index 0000000..68e8408 --- /dev/null +++ b/tests/manual/auth-db-debug.js @@ -0,0 +1,83 @@ +// Simple test to verify auth database functionality +// Run this in browser console at http://localhost:5174 + +console.log('Testing Authentication Database...'); + +// Test the mock database service +async function testDatabase() { + try { + // Import the services (this would work in browser context) + const { dbService } = await import('./services/couchdb.ts'); + const { authService } = await import('./services/auth/auth.service.ts'); + + console.log('1. Testing user creation with password...'); + + // Test creating a user with password + const testEmail = 'test@example.com'; + const testPassword = 'password123'; + + try { + const newUser = await dbService.createUserWithPassword( + testEmail, + testPassword + ); + console.log('✅ User created successfully:', newUser); + } catch (error) { + if (error.message.includes('already exists')) { + console.log('ℹ️ User already exists, testing login...'); + } else { + console.error('❌ User creation failed:', error); + return; + } + } + + console.log('2. Testing password login...'); + + // Test login with password + try { + const loginResult = await authService.login({ + email: testEmail, + password: testPassword, + }); + console.log('✅ Password login successful:', loginResult); + } catch (error) { + console.error('❌ Password login failed:', error); + } + + console.log('3. Testing OAuth user creation...'); + + // Test OAuth user creation + const oauthData = { + email: 'oauth@example.com', + username: 'oauth_user', + avatar: 'https://example.com/avatar.jpg', + }; + + try { + const oauthUser = await dbService.createUserFromOAuth(oauthData); + console.log('✅ OAuth user created successfully:', oauthUser); + } catch (error) { + if (error.message.includes('already exists')) { + console.log('ℹ️ OAuth user already exists'); + } else { + console.error('❌ OAuth user creation failed:', error); + } + } + + console.log('4. Testing user lookup by email...'); + + // Test finding users + const foundUser = await dbService.findUserByEmail(testEmail); + console.log('✅ User found by email:', foundUser); + + console.log('🎉 All database tests completed!'); + } catch (error) { + console.error('💥 Test setup failed:', error); + } +} + +// Export for manual testing +if (typeof window !== 'undefined') { + window.testAuthDB = testDatabase; + console.log('Run window.testAuthDB() to test the authentication database'); +} diff --git a/tests/manual/debug-email-validation.js b/tests/manual/debug-email-validation.js new file mode 100644 index 0000000..39f2df2 --- /dev/null +++ b/tests/manual/debug-email-validation.js @@ -0,0 +1,22 @@ +// Test the email validation in browser console +console.log('Testing email validation for admin@localhost'); + +const emailRegex = /^[^\s@]+@[^\s@]+(\.[^\s@]+|localhost)$/; +const testEmail = 'admin@localhost'; + +console.log('Email:', testEmail); +console.log('Regex:', emailRegex.toString()); +console.log('Test result:', emailRegex.test(testEmail)); + +// Let's also test step by step +console.log('Parts breakdown:'); +console.log('- Has @ symbol:', testEmail.includes('@')); +console.log('- Before @:', testEmail.split('@')[0]); +console.log('- After @:', testEmail.split('@')[1]); +console.log('- No spaces:', !/\s/.test(testEmail)); + +// Let's test a simpler regex that should definitely work +const simpleRegex = /^[^@\s]+@[^@\s]+$/; +console.log('Simple regex test:', simpleRegex.test(testEmail)); + +// Copy this code and paste it in the browser console when you get the validation error diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..06e54e5 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,39 @@ +// Test setup file +// Configure jsdom and global test utilities + +import 'jest-environment-jsdom'; + +// Mock localStorage +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), + length: 0, + key: jest.fn(), +} as Storage; + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, +}); + +// Mock fetch +global.fetch = jest.fn(); + +// Setup console to avoid noise in tests +const originalError = console.error; +beforeAll(() => { + console.error = (...args: any[]) => { + if ( + typeof args[0] === 'string' && + args[0].includes('Warning: ReactDOM.render is deprecated') + ) { + return; + } + originalError.call(console, ...args); + }; +}); + +afterAll(() => { + console.error = originalError; +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..00eb9ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "types": ["node", "jest", "@playwright/test"], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": ["./*"] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..2a99f3b --- /dev/null +++ b/types.ts @@ -0,0 +1,114 @@ +import { AccountStatus } from './services/auth/auth.constants'; + +export interface CouchDBDocument { + _id: string; + _rev: string; +} + +export enum UserRole { + USER = 'USER', + ADMIN = 'ADMIN', +} + +export interface User extends CouchDBDocument { + username: string; + avatar?: string; // Base64 encoded image + email?: string; + password?: string; // For email-based authentication + emailVerified?: boolean; + status?: AccountStatus; // Account status: PENDING, ACTIVE, SUSPENDED + role?: UserRole; // User role for admin functionality + createdAt?: Date; + lastLoginAt?: Date; +} + +export enum Frequency { + Daily = 'Daily', + TwiceADay = 'Twice a day', + ThreeTimesADay = 'Three times a day', + EveryXHours = 'Every X hours', +} + +export interface Medication extends CouchDBDocument { + name: string; + dosage: string; + frequency: Frequency; + hoursBetween?: number; + startTime: string; // "HH:mm" format + notes?: string; + icon?: string; +} + +export interface Dose { + id: string; // A unique ID for the dose instance, e.g., `${medicationId}-${isoDate}` + medicationId: string; + scheduledTime: Date; +} + +export enum DoseStatus { + UPCOMING = 'UPCOMING', + TAKEN = 'TAKEN', + MISSED = 'MISSED', + SNOOZED = 'SNOOZED', +} + +export interface HistoricalDose { + id: string; + medication: Medication; + scheduledTime: Date; + status: DoseStatus; + takenAt?: string; +} + +export interface UserSettings extends CouchDBDocument { + notificationsEnabled: boolean; + hasCompletedOnboarding: boolean; +} + +export interface TakenDoses extends CouchDBDocument { + doses: Record; // key: dose.id, value: ISO timestamp +} + +export interface CustomReminder extends CouchDBDocument { + title: string; + icon: string; + frequencyMinutes: number; + startTime: string; // "HH:mm" + endTime: string; // "HH:mm" +} + +// Represents a single occurrence of a custom reminder on a given day +export interface ReminderInstance { + id: string; // e.g., `${reminderId}-${isoDate}-${time}` + reminderId: string; + title: string; + icon: string; + scheduledTime: Date; +} + +export type ScheduleItem = Dose | ReminderInstance; + +export interface DailyStat { + date: string; // YYYY-MM-DD + adherence: number; // 0-100 +} + +export interface MedicationStat { + medication: Medication; + taken: number; + missed: number; + upcoming: number; + adherence: number; // 0-100 + lastTakenAt?: string; +} + +export enum OAuthProvider { + GOOGLE = 'google', + GITHUB = 'github', +} + +export interface OAuthState { + provider: OAuthProvider; + redirectUri: string; + state: string; +} diff --git a/types/playwright.d.ts b/types/playwright.d.ts new file mode 100644 index 0000000..db97c2e --- /dev/null +++ b/types/playwright.d.ts @@ -0,0 +1,82 @@ +// Temporary type declarations for Playwright +// This file can be removed once @playwright/test is properly installed + +declare module '@playwright/test' { + export interface Page { + goto(url: string): Promise; + click(selector: string): Promise; + fill(selector: string, value: string): Promise; + selectOption(selector: string, value: string): Promise; + locator(selector: string): Locator; + waitForSelector( + selector: string, + options?: { timeout?: number } + ): Promise; + setViewportSize(size: { width: number; height: number }): Promise; + } + + export interface Locator { + click(): Promise; + fill(value: string): Promise; + toBeVisible(): Promise; + toContainText(text: string | string[]): Promise; + toHaveClass(pattern: RegExp): Promise; + not: Locator; + first(): Locator; + toHaveCount(count: number): Promise; + } + + export interface TestFunction { + (name: string, fn: ({ page }: { page: Page }) => Promise): void; + describe: (name: string, fn: () => void) => void; + beforeEach: (fn: ({ page }: { page: Page }) => Promise) => void; + extend: (fixtures: any) => TestFunction; + } + + export interface ExpectFunction { + (actual: any): { + toBeVisible(): Promise; + toContainText(text: string | string[]): Promise; + toHaveClass(pattern: RegExp): Promise; + not: { + toBeVisible(): Promise; + toHaveClass(pattern: RegExp): Promise; + }; + toHaveCount(count: number): Promise; + }; + } + + export const test: TestFunction; + export const expect: ExpectFunction; + + export interface Config { + testDir?: string; + fullyParallel?: boolean; + forbidOnly?: boolean; + retries?: number; + workers?: number; + reporter?: string; + use?: { + baseURL?: string; + trace?: string; + screenshot?: string; + video?: string; + }; + projects?: Array<{ + name: string; + use: any; + }>; + webServer?: { + command: string; + url: string; + reuseExistingServer: boolean; + timeout: number; + }; + } + + export function defineConfig(config: Config): Config; + + export const devices: { + [key: string]: any; + }; +} diff --git a/utils/schedule.ts b/utils/schedule.ts new file mode 100644 index 0000000..50e187f --- /dev/null +++ b/utils/schedule.ts @@ -0,0 +1,107 @@ +import { + Medication, + Dose, + Frequency, + CustomReminder, + ReminderInstance, +} from '../types'; + +export const generateSchedule = ( + medications: Medication[], + forDate: Date +): Dose[] => { + const schedule: Dose[] = []; + const today = new Date(forDate); + today.setHours(0, 0, 0, 0); + + medications.forEach(med => { + const [startHour, startMinute] = med.startTime.split(':').map(Number); + + const createDose = (hour: number, minute: number): Dose => { + const scheduledTime = new Date(today); + scheduledTime.setHours(hour, minute, 0, 0); + const dateString = scheduledTime.toISOString().split('T')[0]; + const timeString = scheduledTime + .toTimeString() + .split(' ')[0] + .replace(/:/g, ''); + return { + id: `${med._id}-${dateString}-${timeString}`, + medicationId: med._id, + scheduledTime, + }; + }; + + switch (med.frequency) { + case Frequency.Daily: + schedule.push(createDose(startHour, startMinute)); + break; + case Frequency.TwiceADay: + schedule.push(createDose(startHour, startMinute)); + schedule.push(createDose((startHour + 12) % 24, startMinute)); + break; + case Frequency.ThreeTimesADay: + schedule.push(createDose(startHour, startMinute)); + schedule.push(createDose((startHour + 8) % 24, startMinute)); + schedule.push(createDose((startHour + 16) % 24, startMinute)); + break; + case Frequency.EveryXHours: + if (med.hoursBetween && med.hoursBetween > 0) { + for (let h = 0; h < 24; h += med.hoursBetween) { + schedule.push(createDose((startHour + h) % 24, startMinute)); + } + } + break; + } + }); + + return schedule.sort( + (a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime() + ); +}; + +export const generateReminderSchedule = ( + reminders: CustomReminder[], + forDate: Date +): ReminderInstance[] => { + const schedule: ReminderInstance[] = []; + const today = new Date(forDate); + today.setHours(0, 0, 0, 0); + + reminders.forEach(reminder => { + const [startHour, startMinute] = reminder.startTime.split(':').map(Number); + const [endHour, endMinute] = reminder.endTime.split(':').map(Number); + + const firstReminderTime = new Date(today); + firstReminderTime.setHours(startHour, startMinute, 0, 0); + + const endOfWindowTime = new Date(today); + endOfWindowTime.setHours(endHour, endMinute, 0, 0); + + let currentReminderTime = firstReminderTime; + + while (currentReminderTime.getTime() <= endOfWindowTime.getTime()) { + const dateString = currentReminderTime.toISOString().split('T')[0]; + const timeString = currentReminderTime + .toTimeString() + .split(' ')[0] + .replace(/:/g, ''); + + schedule.push({ + id: `${reminder._id}-${dateString}-${timeString}`, + reminderId: reminder._id, + title: reminder.title, + icon: reminder.icon, + scheduledTime: new Date(currentReminderTime), + }); + + currentReminderTime = new Date( + currentReminderTime.getTime() + reminder.frequencyMinutes * 60000 + ); + } + }); + + return schedule.sort( + (a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime() + ); +}; diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..523bfeb --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,17 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + }, + }, + }; +});