diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b0e6885..5f4dc526 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,12 @@ jobs: ruff: ${{ steps.changes.outputs.ruff }} steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 - uses: dorny/paths-filter@v3 id: changes with: + base: main filters: | backend: - 'backend/**' diff --git a/.github/workflows/depcheck.yml b/.github/workflows/depcheck.yml new file mode 100644 index 00000000..7a479c00 --- /dev/null +++ b/.github/workflows/depcheck.yml @@ -0,0 +1,346 @@ +# Dependency Check Workflow +# Detects unused and vulnerable dependencies in the project +# Run on: Pull requests and weekly schedule + +name: Dependency Check + +on: + workflow_dispatch: + inputs: + full_audit: + description: 'Run full audit including all dependencies' + required: false + default: 'false' + type: boolean + schedule: + # Run weekly on Sunday at 3 AM UTC + - cron: '0 3 * * 0' + pull_request: + paths: + - '**/package.json' + - '**/package-lock.json' + - '**/pnpm-lock.yaml' + - '**/requirements*.txt' + - '**/pyproject.toml' + - '.github/workflows/depcheck.yml' + +# Cancel in-progress runs for the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Detect which parts of the project have changed + changes: + name: Detect Changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + backend: ${{ steps.filter.outputs.backend }} + ai-engine: ${{ steps.filter.outputs.ai-engine }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Filter paths + id: filter + run: | + echo "Checking for changes in frontend, backend, and ai-engine..." + + # Check frontend changes + if git diff --name-only main...HEAD | grep -q "frontend/"; then + echo "frontend=true" >> $GITHUB_OUTPUT + else + echo "frontend=false" >> $GITHUB_OUTPUT + fi + + # Check backend changes + if git diff --name-only main...HEAD | grep -q "backend/"; then + echo "backend=true" >> $GITHUB_OUTPUT + else + echo "backend=false" >> $GITHUB_OUTPUT + fi + + # Check ai-engine changes + if git diff --name-only main...HEAD | grep -q "ai-engine/"; then + echo "ai-engine=true" >> $GITHUB_OUTPUT + else + echo "ai-engine=false" >> $GITHUB_OUTPUT + fi + + echo "Frontend changed: ${{ steps.filter.outputs.frontend }}" + echo "Backend changed: ${{ steps.filter.outputs.backend }}" + echo "AI-Engine changed: ${{ steps.filter.outputs.ai-engine }}" + + # Frontend: depcheck for npm/TypeScript dependencies + depcheck-frontend: + name: Depcheck - Frontend + runs-on: ubuntu-latest + needs: changes + if: ${{ needs.changes.outputs.frontend == 'true' || github.event.inputs.full_audit == 'true' }} + timeout-minutes: 15 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: | + cd frontend + pnpm install --frozen-lockfile + + - name: Run depcheck + id: depcheck + continue-on-error: true + run: | + cd frontend + echo "Running depcheck to find unused dependencies..." + + # Run depcheck and capture output + if pnpm depcheck 2>&1; then + echo "✅ No unused dependencies found" + echo "unused_deps_found=false" >> $GITHUB_OUTPUT + else + echo "❌ Unused dependencies detected!" + echo "unused_deps_found=true" >> $GITHUB_OUTPUT + fi + + - name: Run npm audit + id: npm-audit + continue-on-error: true + run: | + cd frontend + echo "Running npm audit to check for vulnerabilities..." + + # Run npm audit and capture output + if pnpm audit --audit-level=moderate 2>&1 | tee /tmp/npm-audit-output; then + echo "✅ No vulnerabilities found" + echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT + else + echo "❌ Vulnerabilities detected!" + echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT + fi + + - name: Report results + if: always() + run: | + if [ "${{ steps.depcheck.outputs.unused_deps_found }}" = "true" ]; then + echo "## ⚠️ Unused Dependencies Detected in Frontend" + echo "depcheck found unused dependencies in the frontend" + echo "Please review and remove unused packages from package.json" + else + echo "✅ Frontend depcheck passed - no unused dependencies" + fi + + if [ "${{ steps.npm-audit.outputs.vulnerabilities_found }}" = "true" ]; then + echo "## ⚠️ Vulnerabilities Detected in Frontend" + echo "npm audit found security vulnerabilities" + echo "Please review and update dependencies to fix vulnerabilities" + else + echo "✅ Frontend npm audit passed - no vulnerabilities found" + fi + + # Backend: pip-audit for Python dependencies + pip-audit-backend: + name: Pip Audit & Deptree - Backend + runs-on: ubuntu-latest + needs: changes + if: ${{ needs.changes.outputs.backend == 'true' || github.event.inputs.full_audit == 'true' }} + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install pip-audit and pipdeptree + run: | + pip install pip-audit pipdeptree + + - name: Run pip-audit and pipdeptree + id: pip-audit + run: | + cd backend + echo "Running pip-audit to check for vulnerabilities and unused dependencies..." + + # First, install dependencies + pip install -r requirements.txt -r requirements-dev.txt + + # Run pip-audit to check for vulnerabilities + if pip-audit --strict; then + echo "✅ No vulnerabilities found in dependencies" + echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT + else + echo "❌ Vulnerabilities detected in dependencies!" + echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT + fi + + # Run pipdeptree to check for unused dependencies + echo "" + echo "Running pipdeptree to check for unused dependencies..." + if pipdeptree --warn fail; then + echo "✅ No unused (undepended) packages found" + echo "unused_deps_found=false" >> $GITHUB_OUTPUT + else + echo "❌ Unused dependencies detected!" + echo "unused_deps_found=true" >> $GITHUB_OUTPUT + fi + + - name: Report results + if: always() + run: | + if [ "${{ steps.pip-audit.outputs.vulnerabilities_found }}" = "true" ]; then + echo "## ⚠️ Vulnerabilities Detected in Backend Dependencies" + echo "Please review and update dependencies to fix vulnerabilities" + else + echo "✅ Backend pip-audit passed - no vulnerabilities found" + fi + + if [ "${{ steps.pip-audit.outputs.unused_deps_found }}" = "true" ]; then + echo "## ⚠️ Unused Dependencies Detected in Backend" + echo "pipdeptree found packages that are not dependencies of any other package" + echo "Please review and remove unused packages from requirements.txt" + else + echo "✅ Backend pipdeptree passed - no unused dependencies" + fi + + # AI-Engine: pip-audit for Python dependencies + pip-audit-ai-engine: + name: Pip Audit & Deptree - AI Engine + runs-on: ubuntu-latest + needs: changes + if: ${{ needs.changes.outputs.ai-engine == 'true' || github.event.inputs.full_audit == 'true' }} + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install pip-audit and pipdeptree + run: | + pip install pip-audit pipdeptree + + - name: Run pip-audit and pipdeptree + id: pip-audit-ai + run: | + cd ai-engine + echo "Running pip-audit to check for vulnerabilities and unused dependencies..." + + # First, install dependencies + pip install -r requirements.txt -r requirements-dev.txt + + # Run pip-audit to check for vulnerabilities + if pip-audit --strict; then + echo "✅ No vulnerabilities found in dependencies" + echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT + else + echo "❌ Vulnerabilities detected in dependencies!" + echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT + fi + + # Run pipdeptree to check for unused dependencies + echo "" + echo "Running pipdeptree to check for unused dependencies..." + if pipdeptree --warn fail; then + echo "✅ No unused (undepended) packages found" + echo "unused_deps_found=false" >> $GITHUB_OUTPUT + else + echo "❌ Unused dependencies detected!" + echo "unused_deps_found=true" >> $GITHUB_OUTPUT + fi + + - name: Report results + if: always() + run: | + if [ "${{ steps.pip-audit-ai.outputs.vulnerabilities_found }}" = "true" ]; then + echo "## ⚠️ Vulnerabilities Detected in AI Engine Dependencies" + echo "Please review and update dependencies to fix vulnerabilities" + else + echo "✅ AI Engine pip-audit passed - no vulnerabilities found" + fi + + if [ "${{ steps.pip-audit-ai.outputs.unused_deps_found }}" = "true" ]; then + echo "## ⚠️ Unused Dependencies Detected in AI Engine" + echo "pipdeptree found packages that are not dependencies of any other package" + echo "Please review and remove unused packages from requirements.txt" + else + echo "✅ AI Engine pipdeptree passed - no unused dependencies" + fi + + # Summary + dependency-check-summary: + name: Dependency Check Summary + runs-on: ubuntu-latest + needs: [changes, depcheck-frontend, pip-audit-backend, pip-audit-ai-engine] + if: always() + timeout-minutes: 5 + steps: + - name: Summary + run: | + echo "## Dependency Check Summary" + echo "" + + # Frontend results + echo "### Frontend" + if [ "${{ needs.depcheck-frontend.result }}" == "success" ]; then + echo "✅ Depcheck: Passed" + elif [ "${{ needs.depcheck-frontend.result }}" == "skipped" ]]; then + echo "⏭️ Depcheck: Skipped (no changes detected)" + else + echo "❌ Depcheck: Failed" + fi + echo "" + + # Backend results + echo "### Backend" + if [ "${{ needs.pip-audit-backend.result }}" == "success" ]; then + echo "✅ Pip Audit & Deptree: Passed" + elif [ "${{ needs.pip-audit-backend.result }}" == "skipped" ]; then + echo "⏭️ Pip Audit & Deptree: Skipped (no changes detected)" + else + echo "❌ Pip Audit & Deptree: Failed" + fi + echo "" + + # AI-Engine results + echo "### AI Engine" + if [ "${{ needs.pip-audit-ai-engine.result }}" == "success" ]; then + echo "✅ Pip Audit & Deptree: Passed" + elif [ "${{ needs.pip-audit-ai-engine.result }}" == "skipped" ]; then + echo "⏭️ Pip Audit & Deptree: Skipped (no changes detected)" + else + echo "❌ Pip Audit & Deptree: Failed" + fi + echo "" + + # Overall status + echo "---" + if [ "${{ needs.depcheck-frontend.result }}" != "failure" ] && \ + [ "${{ needs.pip-audit-backend.result }}" != "failure" ] && \ + [ "${{ needs.pip-audit-ai-engine.result }}" != "failure" ]; then + echo "🎉 All dependency checks passed!" + exit 0 + else + echo "⚠️ Some dependency checks failed. Please review the results above." + exit 1 + fi diff --git a/ai-engine/main.py b/ai-engine/main.py index f20a88bb..b9b75a14 100644 --- a/ai-engine/main.py +++ b/ai-engine/main.py @@ -15,6 +15,9 @@ from dotenv import load_dotenv import redis.asyncio as aioredis +# Import tracing module +from tracing import init_tracing, shutdown_tracing + # Configure logging using centralized configuration from utils.logging_config import setup_logging, get_agent_logger, configure_structlog @@ -198,6 +201,10 @@ async def startup_event(): logger.info("Starting ModPorter AI Engine...") + # Initialize tracing + init_tracing(app=app, service_name="modporter-ai-engine") + logger.info("Distributed tracing initialized") + try: # Initialize Redis connection redis_url = os.getenv("REDIS_URL", "redis://localhost:6379") @@ -224,6 +231,22 @@ async def startup_event(): logger.error(f"Failed to initialize AI Engine: {e}", exc_info=True) raise HTTPException(status_code=503, detail="Service initialization failed") +@app.on_event("shutdown") +async def shutdown_event(): + """Cleanup on shutdown""" + global redis_client + + # Shutdown tracing + shutdown_tracing() + logger.info("Distributed tracing shutdown") + + # Close Redis connection + if redis_client: + await redis_client.close() + logger.info("Redis connection closed") + + logger.info("ModPorter AI Engine shutdown complete") + @app.get("/api/v1/health", response_model=HealthResponse, tags=["health"]) async def health_check(): """Check the health status of the AI Engine""" diff --git a/ai-engine/pyproject.toml b/ai-engine/pyproject.toml index b3d5a6a0..420bfc86 100644 --- a/ai-engine/pyproject.toml +++ b/ai-engine/pyproject.toml @@ -16,11 +16,15 @@ exclude = [ ] [tool.ruff.lint] -# Enable pycodestyle (E), Pyflakes (F), isort (I), flake8-quotes (Q), flake8-naming (N), pydocstyle (D), pyupgrade (UP) -select = ["E", "F", "I", "Q", "N", "D", "UP"] +# Enable pycodestyle (E), Pyflakes (F), warnings (W), isort (I), flake8-quotes (Q), flake8-naming (N), pydocstyle (D), pyupgrade (UP) +select = ["E", "F", "W", "I", "Q", "N", "D", "UP"] ignore = [ "E402", # module level import not at top of file "E501", # line too long (handled by formatter) + # W (pycodestyle warnings) ignores for legacy code + "W291", # trailing whitespace (legacy code has inconsistent whitespace) + "W292", # no newline at end of file (legacy code has inconsistent newlines) + "W293", # blank line contains whitespace (legacy code has inconsistent blank lines) "Q000", # bad quote marks "Q001", # bad quote marks in multiline strings "Q002", # bad quote marks in strings diff --git a/ai-engine/requirements-dev.txt b/ai-engine/requirements-dev.txt index 93146153..8e90a484 100644 --- a/ai-engine/requirements-dev.txt +++ b/ai-engine/requirements-dev.txt @@ -15,4 +15,8 @@ black>=23.0.0 mypy>=1.5.0 # Development Tools -pre-commit>=3.0.0 \ No newline at end of file +pre-commit>=3.0.0 + +# Dependency checking +pip-audit>=2.7.0 +pipdeptree>=2.23.0 \ No newline at end of file diff --git a/ai-engine/requirements.txt b/ai-engine/requirements.txt index c6f209d9..b96a5b2d 100644 --- a/ai-engine/requirements.txt +++ b/ai-engine/requirements.txt @@ -49,4 +49,13 @@ pydantic-settings # Monitoring prometheus-client psutil -structlog>=24.0.0 \ No newline at end of file +structlog>=24.0.0 + +# Distributed Tracing - OpenTelemetry +opentelemetry-api>=1.24.0 +opentelemetry-sdk>=1.24.0 +opentelemetry-exporter-otlp>=1.24.0 +opentelemetry-exporter-jaeger>=1.24.0 +opentelemetry-instrumentation-fastapi>=0.45b0 +opentelemetry-instrumentation-httpx>=0.45b0 +opentelemetry-instrumentation-redis>=0.45b0 \ No newline at end of file diff --git a/ai-engine/tracing.py b/ai-engine/tracing.py new file mode 100644 index 00000000..d3d80762 --- /dev/null +++ b/ai-engine/tracing.py @@ -0,0 +1,324 @@ +""" +Distributed Tracing Service using OpenTelemetry for AI Engine. + +This module provides tracing capabilities for the AI Engine service, +including trace context propagation between services. +""" + +import os +from typing import Optional +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.jaeger.thrift import JaegerExporter +from opentelemetry.sdk.extension.aws.resource.ec2 import AwsEc2ResourceDetector +from opentelemetry.sdk.extension.aws.resource.ecs import AwsEcsResourceDetector +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from opentelemetry.trace import Status, StatusCode +from opentelemetry.context import Context +import logging + +logger = logging.getLogger(__name__) + +# Trace context propagator (W3C Trace Context) +tracer_propagator = TraceContextTextMapPropagator() + +# Global tracer instance +_tracer: Optional[trace.Tracer] = None +_tracer_provider: Optional[TracerProvider] = None + + +def get_tracer(service_name: str = "modporter-ai-engine") -> trace.Tracer: + """ + Get or create a tracer instance for the given service. + + Args: + service_name: Name of the service for tracing + + Returns: + Configured tracer instance + """ + global _tracer, _tracer_provider + + if _tracer is not None: + return _tracer + + # Create resource with service information + service_version = os.getenv("SERVICE_VERSION", "1.0.0") + resource = Resource.create({ + SERVICE_NAME: service_name, + SERVICE_VERSION: service_version, + }) + + # Add cloud metadata if available + try: + ec2_resource = AwsEc2ResourceDetector().detect() + resource = resource.merge(ec2_resource) + except Exception: + pass + + try: + ecs_resource = AwsEcsResourceDetector().detect() + resource = resource.merge(ecs_resource) + except Exception: + pass + + # Create tracer provider + _tracer_provider = TracerProvider(resource=resource) + + # Configure exporter based on environment + tracing_enabled = os.getenv("TRACING_ENABLED", "true").lower() == "true" + tracing_exporter = os.getenv("TRACING_EXPORTER", "jaeger").lower() + + if tracing_enabled: + if tracing_exporter == "jaeger": + # Jaeger exporter configuration + jaeger_host = os.getenv("JAEGER_HOST", "localhost") + jaeger_port = int(os.getenv("JAEGER_PORT", "6831")) + + jaeger_exporter = JaegerExporter( + agent_host_name=jaeger_host, + agent_port=jaeger_port, + ) + _tracer_provider.add_span_processor( + BatchSpanProcessor(jaeger_exporter) + ) + logger.info(f"Jaeger tracing enabled: {jaeger_host}:{jaeger_port}") + + elif tracing_exporter == "otlp": + # OTLP exporter configuration + otlp_endpoint = os.getenv("OTLP_ENDPOINT", "http://localhost:4317") + + otlp_exporter = OTLPSpanExporter( + endpoint=otlp_endpoint, + insecure=True, + ) + _tracer_provider.add_span_processor( + BatchSpanProcessor(otlp_exporter) + ) + logger.info(f"OTLP tracing enabled: {otlp_endpoint}") + + # Add console exporter for development + if os.getenv("TRACING_CONSOLE", "false").lower() == "true": + _tracer_provider.add_span_processor( + BatchSpanProcessor(ConsoleSpanExporter()) + ) + logger.info("Console span exporter enabled") + + # Set the global tracer provider + trace.set_tracer_provider(_tracer_provider) + + # Create and return tracer + _tracer = trace.get_tracer(service_name) + + logger.info(f"Tracing initialized for service: {service_name}") + + return _tracer + + +def init_tracing( + app=None, + service_name: str = "modporter-ai-engine", + instrument_fastapi: bool = True, + instrument_httpx: bool = True, + instrument_redis: bool = True, +) -> trace.Tracer: + """ + Initialize tracing with automatic instrumentation. + + Args: + app: FastAPI application instance (optional) + service_name: Name of the service + instrument_fastapi: Whether to instrument FastAPI + instrument_httpx: Whether to instrument HTTPX + instrument_redis: Whether to instrument Redis + + Returns: + Configured tracer instance + """ + tracer = get_tracer(service_name) + + # Instrument FastAPI if app provided + if app and instrument_fastapi: + try: + FastAPIInstrumentor.instrument_app(app) + logger.info("FastAPI instrumentation enabled") + except Exception as e: + logger.warning(f"Failed to instrument FastAPI: {e}") + + # Instrument HTTPX + if instrument_httpx: + try: + HTTPXClientInstrumentor().instrument() + logger.info("HTTPX instrumentation enabled") + except Exception as e: + logger.warning(f"Failed to instrument HTTPX: {e}") + + # Instrument Redis + if instrument_redis: + try: + RedisInstrumentor().instrument() + logger.info("Redis instrumentation enabled") + except Exception as e: + logger.warning(f"Failed to instrument Redis: {e}") + + return tracer + + +def extract_trace_context(carrier: dict) -> Context: + """ + Extract trace context from carrier (e.g., HTTP headers). + + Args: + carrier: Dictionary containing trace context (e.g., HTTP headers) + + Returns: + Extracted context + """ + return tracer_propagator.extract(carrier) + + +def inject_trace_context(carrier: dict) -> dict: + """ + Inject trace context into carrier (e.g., HTTP headers). + + Args: + carrier: Dictionary to inject trace context into + + Returns: + Carrier with injected trace context + """ + tracer_propagator.inject(carrier) + return carrier + + +def create_span( + name: str, + context: Optional[Context] = None, + kind: trace.SpanKind = trace.SpanKind.INTERNAL, +) -> trace.Span: + """ + Create a new span with the given name and context. + + Args: + name: Name of the span + context: Parent context (optional) + kind: Span kind + + Returns: + New span + """ + tracer = get_tracer() + + if context: + with tracer.start_as_current_span(name, context=context, kind=kind) as span: + return span + else: + with tracer.start_as_current_span(name, kind=kind) as span: + return span + + +def add_span_attributes(span: trace.Span, attributes: dict) -> None: + """ + Add attributes to a span. + + Args: + span: Span to add attributes to + attributes: Dictionary of attributes + """ + for key, value in attributes.items(): + if value is not None: + span.set_attribute(key, str(value)) + + +def record_span_exception(span: trace.Span, exception: Exception) -> None: + """ + Record an exception on a span. + + Args: + span: Span to record exception on + exception: Exception to record + """ + span.set_status(Status(StatusCode.ERROR, str(exception))) + span.record_exception(exception) + + +def shutdown_tracing() -> None: + """Shutdown the tracing provider and flush any pending spans.""" + global _tracer_provider + + if _tracer_provider: + _tracer_provider.shutdown() + logger.info("Tracing provider shutdown") + + +class TracingMiddleware: + """ + Middleware for FastAPI to handle trace context propagation. + + This middleware extracts trace context from incoming requests + and injects it into outgoing requests. + """ + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + # Extract trace context from headers + headers = dict(scope.get("headers", [])) + # Convert bytes to string for headers + headers = {k.decode(): v.decode() for k, v in headers.items()} + + # The FastAPI instrumentation will handle this automatically, + # but we keep this for custom use cases + context = extract_trace_context(headers) + + # Continue with the request + await self.app(scope, receive, send) + + +def get_current_span() -> Optional[trace.Span]: + """ + Get the current active span if any. + + Returns: + Current span or None + """ + return trace.get_current_span() + + +def get_trace_id() -> Optional[str]: + """ + Get the current trace ID as a hex string. + + Returns: + Trace ID or None + """ + span = get_current_span() + if span: + trace_id = span.get_span_context().trace_id + return format(trace_id, '032x') + return None + + +def get_span_id() -> Optional[str]: + """ + Get the current span ID as a hex string. + + Returns: + Span ID or None + """ + span = get_current_span() + if span: + span_id = span.get_span_context().span_id + return format(span_id, '016x') + return None diff --git a/ai-engine/utils/logging_config.py b/ai-engine/utils/logging_config.py index 713c5c0e..c6b09ef0 100644 --- a/ai-engine/utils/logging_config.py +++ b/ai-engine/utils/logging_config.py @@ -58,12 +58,15 @@ def configure_structlog( log_dir = os.getenv("LOG_DIR", "/tmp/modporter-ai/logs") # Configure processors based on format + # Order matters: context merging -> logger info -> level -> timestamper -> renderer -> exception handling processors = [ structlog.contextvars.merge_contextvars, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, ] if debug_mode: @@ -73,10 +76,6 @@ def configure_structlog( else: processors.append(structlog.dev.ConsoleRenderer(colors=False)) - # Add exception info processor - processors.append(structlog.processors.StackInfoRenderer()) - processors.append(structlog.processors.format_exc_info) - # Configure structlog structlog.configure( processors=processors, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 63f8b0e3..524d5bb9 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -73,11 +73,15 @@ exclude = [ ] [tool.ruff.lint] -# Enable pycodestyle (E), Pyflakes (F), isort (I), flake8-quotes (Q), flake8-naming (N), pydocstyle (D), pyupgrade (UP) -select = ["E", "F", "I", "Q", "N", "D", "UP"] +# Enable pycodestyle (E), Pyflakes (F), warnings (W), isort (I), flake8-quotes (Q), flake8-naming (N), pydocstyle (D), pyupgrade (UP) +select = ["E", "F", "W", "I", "Q", "N", "D", "UP"] ignore = [ "E402", # module level import not at top of file "E501", # line too long (handled by formatter) + # W (pycodestyle warnings) ignores for legacy code + "W291", # trailing whitespace (legacy code has inconsistent whitespace) + "W292", # no newline at end of file (legacy code has inconsistent newlines) + "W293", # blank line contains whitespace (legacy code has inconsistent blank lines) "Q000", # bad quote marks "Q001", # bad quote marks in multiline strings "Q002", # bad quote marks in strings diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 0d08d43f..fb588267 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -13,3 +13,7 @@ pre-commit>=3.0.0 # Logging structlog + +# Dependency checking +pip-audit>=2.7.0 +pipdeptree>=2.23.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index 30d6bc8e..b260824b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -33,6 +33,15 @@ prometheus_client>=0.17.0 sentry-sdk[fastapi]>=2.0.0 structlog>=24.0.0 +# Distributed Tracing - OpenTelemetry +opentelemetry-api>=1.24.0 +opentelemetry-sdk>=1.24.0 +opentelemetry-exporter-otlp>=1.24.0 +opentelemetry-exporter-jaeger>=1.24.0 +opentelemetry-instrumentation-fastapi>=0.45b0 +opentelemetry-instrumentation-httpx>=0.45b0 +opentelemetry-instrumentation-redis>=0.45b0 + # Testing pytest>=8.2 pytest-asyncio==1.3.0 diff --git a/backend/src/main.py b/backend/src/main.py index de2abfb6..70882cb7 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -48,6 +48,7 @@ ) from services.security_headers import SecurityHeadersMiddleware from services.logging_middleware import LoggingMiddleware, RequestContextMiddleware +from services.tracing import init_tracing, shutdown_tracing # Import API routers from api import ( @@ -101,8 +102,15 @@ async def lifespan(app: FastAPI): if testing_env != "true": await init_db() logger.info("Database initialized") + + # Initialize tracing + init_tracing(app=app, service_name="modporter-backend") + logger.info("Distributed tracing initialized") + yield + # Shutdown + shutdown_tracing() logger.info("Application shutdown") diff --git a/backend/src/services/ai_engine_client.py b/backend/src/services/ai_engine_client.py index e517e5aa..5eb077aa 100644 --- a/backend/src/services/ai_engine_client.py +++ b/backend/src/services/ai_engine_client.py @@ -3,6 +3,7 @@ Provides HTTP client for communicating with the AI Engine API. Handles file transfers, conversion requests, and progress polling. +Includes distributed tracing support. """ import asyncio @@ -13,6 +14,8 @@ import httpx +from services.tracing import inject_trace_context, get_trace_id + logger = logging.getLogger(__name__) # AI Engine configuration @@ -63,6 +66,21 @@ async def _get_client(self) -> httpx.AsyncClient: ) return self._client + def _get_trace_headers(self) -> Dict[str, str]: + """ + Get trace context headers for propagating to downstream services. + + Returns: + Dictionary of trace headers to include in requests + """ + headers = {} + inject_trace_context(headers) + # Also add trace_id for logging purposes + trace_id = get_trace_id() + if trace_id: + headers["X-Trace-ID"] = trace_id + return headers + async def close(self): """Close the HTTP client.""" if self._client and not self._client.is_closed: @@ -78,7 +96,8 @@ async def health_check(self) -> bool: """ try: client = await self._get_client() - response = await client.get("/api/v1/health") + headers = self._get_trace_headers() + response = await client.get("/api/v1/health", headers=headers) return response.status_code == 200 except Exception as e: logger.warning(f"AI Engine health check failed: {e}") @@ -108,6 +127,7 @@ async def start_conversion( """ try: client = await self._get_client() + headers = self._get_trace_headers() request_data = { "job_id": job_id, @@ -121,6 +141,7 @@ async def start_conversion( response = await client.post( "/api/v1/convert", json=request_data, + headers=headers, ) if response.status_code != 200: @@ -161,7 +182,8 @@ async def get_conversion_status(self, job_id: str) -> Dict[str, Any]: """ try: client = await self._get_client() - response = await client.get(f"/api/v1/status/{job_id}") + headers = self._get_trace_headers() + response = await client.get(f"/api/v1/status/{job_id}", headers=headers) if response.status_code == 404: raise AIEngineError("Job not found", status_code=404) diff --git a/backend/src/services/structured_logging.py b/backend/src/services/structured_logging.py index 3ea81f2b..22080f19 100644 --- a/backend/src/services/structured_logging.py +++ b/backend/src/services/structured_logging.py @@ -56,12 +56,15 @@ def configure_structlog( log_dir = os.getenv("LOG_DIR", "/var/log/modporter") # Configure processors based on format + # Order matters: context merging -> logger info -> level -> timestamper -> renderer -> exception handling processors = [ structlog.contextvars.merge_contextvars, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, ] if debug_mode: @@ -71,10 +74,6 @@ def configure_structlog( else: processors.append(structlog.dev.ConsoleRenderer(colors=False)) - # Add exception info processor - processors.append(structlog.processors.StackInfoRenderer()) - processors.append(structlog.processors.format_exc_info) - # Configure structlog structlog.configure( processors=processors, diff --git a/backend/src/services/tracing.py b/backend/src/services/tracing.py new file mode 100644 index 00000000..2f6b94ae --- /dev/null +++ b/backend/src/services/tracing.py @@ -0,0 +1,324 @@ +""" +Distributed Tracing Service using OpenTelemetry. + +This module provides tracing capabilities for the ModPorter AI application, +including trace context propagation between services. +""" + +import os +from typing import Optional +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.jaeger.thrift import JaegerExporter +from opentelemetry.sdk.extension.aws.resource.ec2 import AwsEc2ResourceDetector +from opentelemetry.sdk.extension.aws.resource.ecs import AwsEcsResourceDetector +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from opentelemetry.trace import Status, StatusCode +from opentelemetry.context import Context +import logging + +logger = logging.getLogger(__name__) + +# Trace context propagator (W3C Trace Context) +tracer_propagator = TraceContextTextMapPropagator() + +# Global tracer instance +_tracer: Optional[trace.Tracer] = None +_tracer_provider: Optional[TracerProvider] = None + + +def get_tracer(service_name: str = "modporter-backend") -> trace.Tracer: + """ + Get or create a tracer instance for the given service. + + Args: + service_name: Name of the service for tracing + + Returns: + Configured tracer instance + """ + global _tracer, _tracer_provider + + if _tracer is not None: + return _tracer + + # Create resource with service information + service_version = os.getenv("SERVICE_VERSION", "1.0.0") + resource = Resource.create({ + SERVICE_NAME: service_name, + SERVICE_VERSION: service_version, + }) + + # Add cloud metadata if available + try: + ec2_resource = AwsEc2ResourceDetector().detect() + resource = resource.merge(ec2_resource) + except Exception: + pass + + try: + ecs_resource = AwsEcsResourceDetector().detect() + resource = resource.merge(ecs_resource) + except Exception: + pass + + # Create tracer provider + _tracer_provider = TracerProvider(resource=resource) + + # Configure exporter based on environment + tracing_enabled = os.getenv("TRACING_ENABLED", "true").lower() == "true" + tracing_exporter = os.getenv("TRACING_EXPORTER", "jaeger").lower() + + if tracing_enabled: + if tracing_exporter == "jaeger": + # Jaeger exporter configuration + jaeger_host = os.getenv("JAEGER_HOST", "localhost") + jaeger_port = int(os.getenv("JAEGER_PORT", "6831")) + + jaeger_exporter = JaegerExporter( + agent_host_name=jaeger_host, + agent_port=jaeger_port, + ) + _tracer_provider.add_span_processor( + BatchSpanProcessor(jaeger_exporter) + ) + logger.info(f"Jaeger tracing enabled: {jaeger_host}:{jaeger_port}") + + elif tracing_exporter == "otlp": + # OTLP exporter configuration + otlp_endpoint = os.getenv("OTLP_ENDPOINT", "http://localhost:4317") + + otlp_exporter = OTLPSpanExporter( + endpoint=otlp_endpoint, + insecure=True, + ) + _tracer_provider.add_span_processor( + BatchSpanProcessor(otlp_exporter) + ) + logger.info(f"OTLP tracing enabled: {otlp_endpoint}") + + # Add console exporter for development + if os.getenv("TRACING_CONSOLE", "false").lower() == "true": + _tracer_provider.add_span_processor( + BatchSpanProcessor(ConsoleSpanExporter()) + ) + logger.info("Console span exporter enabled") + + # Set the global tracer provider + trace.set_tracer_provider(_tracer_provider) + + # Create and return tracer + _tracer = trace.get_tracer(service_name) + + logger.info(f"Tracing initialized for service: {service_name}") + + return _tracer + + +def init_tracing( + app=None, + service_name: str = "modporter-backend", + instrument_fastapi: bool = True, + instrument_httpx: bool = True, + instrument_redis: bool = True, +) -> trace.Tracer: + """ + Initialize tracing with automatic instrumentation. + + Args: + app: FastAPI application instance (optional) + service_name: Name of the service + instrument_fastapi: Whether to instrument FastAPI + instrument_httpx: Whether to instrument HTTPX + instrument_redis: Whether to instrument Redis + + Returns: + Configured tracer instance + """ + tracer = get_tracer(service_name) + + # Instrument FastAPI if app provided + if app and instrument_fastapi: + try: + FastAPIInstrumentor.instrument_app(app) + logger.info("FastAPI instrumentation enabled") + except Exception as e: + logger.warning(f"Failed to instrument FastAPI: {e}") + + # Instrument HTTPX + if instrument_httpx: + try: + HTTPXClientInstrumentor().instrument() + logger.info("HTTPX instrumentation enabled") + except Exception as e: + logger.warning(f"Failed to instrument HTTPX: {e}") + + # Instrument Redis + if instrument_redis: + try: + RedisInstrumentor().instrument() + logger.info("Redis instrumentation enabled") + except Exception as e: + logger.warning(f"Failed to instrument Redis: {e}") + + return tracer + + +def extract_trace_context(carrier: dict) -> Context: + """ + Extract trace context from carrier (e.g., HTTP headers). + + Args: + carrier: Dictionary containing trace context (e.g., HTTP headers) + + Returns: + Extracted context + """ + return tracer_propagator.extract(carrier) + + +def inject_trace_context(carrier: dict) -> dict: + """ + Inject trace context into carrier (e.g., HTTP headers). + + Args: + carrier: Dictionary to inject trace context into + + Returns: + Carrier with injected trace context + """ + tracer_propagator.inject(carrier) + return carrier + + +def create_span( + name: str, + context: Optional[Context] = None, + kind: trace.SpanKind = trace.SpanKind.INTERNAL, +) -> trace.Span: + """ + Create a new span with the given name and context. + + Args: + name: Name of the span + context: Parent context (optional) + kind: Span kind + + Returns: + New span + """ + tracer = get_tracer() + + if context: + with tracer.start_as_current_span(name, context=context, kind=kind) as span: + return span + else: + with tracer.start_as_current_span(name, kind=kind) as span: + return span + + +def add_span_attributes(span: trace.Span, attributes: dict) -> None: + """ + Add attributes to a span. + + Args: + span: Span to add attributes to + attributes: Dictionary of attributes + """ + for key, value in attributes.items(): + if value is not None: + span.set_attribute(key, str(value)) + + +def record_span_exception(span: trace.Span, exception: Exception) -> None: + """ + Record an exception on a span. + + Args: + span: Span to record exception on + exception: Exception to record + """ + span.set_status(Status(StatusCode.ERROR, str(exception))) + span.record_exception(exception) + + +def shutdown_tracing() -> None: + """Shutdown the tracing provider and flush any pending spans.""" + global _tracer_provider + + if _tracer_provider: + _tracer_provider.shutdown() + logger.info("Tracing provider shutdown") + + +class TracingMiddleware: + """ + Middleware for FastAPI to handle trace context propagation. + + This middleware extracts trace context from incoming requests + and injects it into outgoing requests. + """ + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + # Extract trace context from headers + headers = dict(scope.get("headers", [])) + # Convert bytes to string for headers + headers = {k.decode(): v.decode() for k, v in headers.items()} + + # The FastAPI instrumentation will handle this automatically, + # but we keep this for custom use cases + context = extract_trace_context(headers) + + # Continue with the request + await self.app(scope, receive, send) + + +def get_current_span() -> Optional[trace.Span]: + """ + Get the current active span if any. + + Returns: + Current span or None + """ + return trace.get_current_span() + + +def get_trace_id() -> Optional[str]: + """ + Get the current trace ID as a hex string. + + Returns: + Trace ID or None + """ + span = get_current_span() + if span: + trace_id = span.get_span_context().trace_id + return format(trace_id, '032x') + return None + + +def get_span_id() -> Optional[str]: + """ + Get the current span ID as a hex string. + + Returns: + Span ID or None + """ + span = get_current_span() + if span: + span_id = span.get_span_context().span_id + return format(span_id, '016x') + return None diff --git a/docker-compose.yml b/docker-compose.yml index 5e3245ad..194bcae8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,11 +26,17 @@ services: - REDIS_URL=redis://redis:6379 - DATABASE_URL=postgresql+asyncpg://postgres:password@postgres:5432/modporter - LOG_LEVEL=INFO + - TRACING_ENABLED=true + - TRACING_EXPORTER=jaeger + - JAEGER_HOST=jaeger + - JAEGER_PORT=6831 depends_on: redis: condition: service_healthy postgres: condition: service_healthy + jaeger: + condition: service_started volumes: - ./backend/src:/app/src - conversion-cache:/app/cache @@ -59,9 +65,15 @@ services: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - LOG_LEVEL=INFO - PORT=8001 + - TRACING_ENABLED=true + - TRACING_EXPORTER=jaeger + - JAEGER_HOST=jaeger + - JAEGER_PORT=6831 depends_on: redis: condition: service_healthy + jaeger: + condition: service_started volumes: - model-cache:/app/models - temp-uploads:/app/temp_uploads @@ -120,6 +132,34 @@ services: retries: 5 start_period: 30s + # Jaeger for distributed tracing visualization + jaeger: + image: jaegertracing/all-in-one:1.52 + ports: + - "6831:6831/udp" # Jaeger thrift compact protocol + - "16686:16686" # Jaeger UI + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + environment: + - COLLECTOR_OTLP_ENABLED=true + - SPAN_STORAGE_TYPE=badger + volumes: + - jaeger-data:/var/lib/jaeger + networks: + - modporter-network + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:16686"] + interval: 30s + timeout: 10s + retries: 3 + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + volumes: redis-data: driver: local @@ -133,6 +173,8 @@ volumes: driver: local conversion-outputs: driver: local + jaeger-data: + driver: local networks: modporter-network: diff --git a/frontend/.depcheckrc b/frontend/.depcheckrc new file mode 100644 index 00000000..2675e99b --- /dev/null +++ b/frontend/.depcheckrc @@ -0,0 +1,26 @@ +# Depcheck configuration for frontend +# Detects unused dependencies in package.json + +{ + "ignoreDirs": [ + "dist", + "node_modules", + ".git", + "coverage", + "e2e" + ], + "ignorePatterns": [ + "*.test.ts", + "*.test.tsx", + "*.spec.ts", + "*.spec.tsx", + "stories", + "**/*.stories.tsx" + ], + "skipMissing": false, + "detective": { + "ts": { + "parser": "typescript" + } + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8c97bc1e..b60fe72b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.3.8", "@mui/material": "^7.3.5", + "@sentry/react": "^8.0.0", "@tanstack/react-query": "^5.90.21", "axios": "^1.13.6", "date-fns": "^4.1.0", @@ -42,6 +43,7 @@ "@vitejs/plugin-react": "^5.1.4", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.7", + "depcheck": "^1.4.7", "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -3740,6 +3742,98 @@ "dev": true, "license": "MIT" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz", + "integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.0.tgz", + "integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.0.tgz", + "integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz", + "integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.0.tgz", + "integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.0", + "@sentry-internal/feedback": "8.55.0", + "@sentry-internal/replay": "8.55.0", + "@sentry-internal/replay-canvas": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.55.0.tgz", + "integrity": "sha512-/qNBvFLpvSa/Rmia0jpKfJdy16d4YZaAnH/TuKLAtm0BWlsPQzbXCU4h8C5Hsst0Do0zG613MEtEmWpWrVOqWA==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "8.55.0", + "@sentry/core": "8.55.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -4531,6 +4625,13 @@ "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", "dev": true }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", @@ -5070,6 +5171,80 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vue/compiler-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "dev": true, + "license": "MIT" + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -5190,6 +5365,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -5213,6 +5398,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -5311,6 +5506,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -5582,6 +5787,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5590,6 +5804,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001770", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", @@ -6577,6 +6804,180 @@ "node": ">=0.4.0" } }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/depcheck/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/depcheck/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/depcheck/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/depcheck/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/depcheck/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", + "dev": true, + "license": "MIT" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6597,6 +6998,16 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", @@ -7428,6 +7839,19 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -7617,6 +8041,22 @@ "micromatch": "^4.0.2" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -7883,6 +8323,51 @@ "node": ">=10.13.0" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "17.3.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", @@ -8064,6 +8549,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -8179,6 +8677,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -8705,6 +9210,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -9079,6 +9594,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -9439,6 +9961,50 @@ } } }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/mutation-server-protocol": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", @@ -9825,6 +10391,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -10024,6 +10600,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -10373,6 +10959,32 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -10486,6 +11098,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -10505,6 +11124,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -10698,6 +11331,13 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -10907,6 +11547,13 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 078f1fb7..ac15c745 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,8 @@ "build-storybook": "storybook build", "storybook:test": "test-storybook", "storybook:build": "storybook build --output-dir dist-storybook", - "mutation": "stryker run" + "mutation": "stryker run", + "depcheck": "depcheck" }, "dependencies": { "@emotion/react": "^11.14.0", @@ -60,6 +61,7 @@ "@vitejs/plugin-react": "^5.1.4", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.7", + "depcheck": "^1.4.7", "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -79,8 +81,6 @@ "typescript-eslint": "^8.56.1", "vite": "7.3.1", "vitest": "^4.0.7", - "web-streams-polyfill": "^4.2.0", - "@stryker-mutator/core": "^9.6.0", - "@stryker-mutator/vitest-runner": "^9.6.0" + "web-streams-polyfill": "^4.2.0" } }