diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml
new file mode 100644
index 0000000..417b368
--- /dev/null
+++ b/.github/workflows/build-and-push.yml
@@ -0,0 +1,34 @@
+name: Build and Push Docker Image
+
+on:
+ push:
+ paths:
+ - 'frontend/**'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: ./frontend
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: morauen/oxn-frontend:latest
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index aca3bd2..ee712ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,15 @@ evaluation.py
*.pickle
kubevpn.exe
venv
+
+# terraform
+terraform.tfstate
+terraform.tfstate.backup
+.terraform.lock.hcl
+.terraform
+
+# logs
+*.log
+
+# Cluster configuration
+config/.cluster-prefix
\ No newline at end of file
diff --git a/backend/db.sqlite3 b/backend/db.sqlite3
new file mode 100644
index 0000000..e69de29
diff --git a/backend/manage.py b/backend/manage.py
new file mode 100644
index 0000000..8926ee0
--- /dev/null
+++ b/backend/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oxn_backend.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/backend/oxn_backend/__init__.py b/backend/oxn_backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/oxn_backend/asgi.py b/backend/oxn_backend/asgi.py
new file mode 100644
index 0000000..8d4f159
--- /dev/null
+++ b/backend/oxn_backend/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for oxn_backend project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oxn_backend.settings')
+
+application = get_asgi_application()
diff --git a/backend/oxn_backend/settings.py b/backend/oxn_backend/settings.py
new file mode 100644
index 0000000..b275b12
--- /dev/null
+++ b/backend/oxn_backend/settings.py
@@ -0,0 +1,124 @@
+"""
+Django settings for oxn_backend project.
+
+Generated by 'django-admin startproject' using Django 5.1.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.1/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-3ex&@i1%1418$p_nw@(vas8z03maau+z29++c*v$x9=wu-&qjk'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'oxn_logic'
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'oxn_backend.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'oxn_backend.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.1/howto/static-files/
+
+STATIC_URL = 'static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
diff --git a/backend/oxn_backend/urls.py b/backend/oxn_backend/urls.py
new file mode 100644
index 0000000..f154947
--- /dev/null
+++ b/backend/oxn_backend/urls.py
@@ -0,0 +1,24 @@
+"""
+URL configuration for oxn_backend project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path, include
+
+
+
+urlpatterns = [
+ path('', include('oxn_logic.urls'))
+]
diff --git a/backend/oxn_backend/wsgi.py b/backend/oxn_backend/wsgi.py
new file mode 100644
index 0000000..98eb892
--- /dev/null
+++ b/backend/oxn_backend/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for oxn_backend project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oxn_backend.settings')
+
+application = get_wsgi_application()
diff --git a/backend/oxn_logic/__init__.py b/backend/oxn_logic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/oxn_logic/admin.py b/backend/oxn_logic/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/backend/oxn_logic/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/backend/oxn_logic/apps.py b/backend/oxn_logic/apps.py
new file mode 100644
index 0000000..3bc9e14
--- /dev/null
+++ b/backend/oxn_logic/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class OxnLogicConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'oxn_logic'
diff --git a/backend/oxn_logic/migrations/__init__.py b/backend/oxn_logic/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/oxn_logic/models.py b/backend/oxn_logic/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/backend/oxn_logic/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/backend/oxn_logic/tests.py b/backend/oxn_logic/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/backend/oxn_logic/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/backend/oxn_logic/urls.py b/backend/oxn_logic/urls.py
new file mode 100644
index 0000000..3d38d29
--- /dev/null
+++ b/backend/oxn_logic/urls.py
@@ -0,0 +1,6 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path('api/helloworld', views.hello_world, name='hello_world'),
+]
\ No newline at end of file
diff --git a/backend/oxn_logic/views.py b/backend/oxn_logic/views.py
new file mode 100644
index 0000000..015afbe
--- /dev/null
+++ b/backend/oxn_logic/views.py
@@ -0,0 +1,7 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+
+# Create your views here.
+
+def hello_world(request):
+ return HttpResponse("Hello world")
diff --git a/backend/readme.md b/backend/readme.md
new file mode 100644
index 0000000..2b8253b
--- /dev/null
+++ b/backend/readme.md
@@ -0,0 +1,12 @@
+# This is the backend Documentation for OXN
+
+1. make sure Django is included into the setup.cfg
+
+2. How to add routes:
+
+ 1. to add routes update the oxn_logic.views.py file with the code
+ 2. map the code to some url in the oxn_logic.urls.py file
+
+3. run the server on localhost from backend directory
+
+ python manage.py runserver
\ No newline at end of file
diff --git a/documentation/Architecute-building-blocks.drawio b/documentation/Architecute-building-blocks.drawio
new file mode 100644
index 0000000..00fa54f
--- /dev/null
+++ b/documentation/Architecute-building-blocks.drawio
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/experiments/recommendation_delay90_A.yml b/experiments/OLD_recommendation_delay90_A.yml
similarity index 100%
rename from experiments/recommendation_delay90_A.yml
rename to experiments/OLD_recommendation_delay90_A.yml
diff --git a/experiments/recommendation_delay90_B.yml b/experiments/OLD_recommendation_delay90_B.yml
similarity index 100%
rename from experiments/recommendation_delay90_B.yml
rename to experiments/OLD_recommendation_delay90_B.yml
diff --git a/experiments/recommendation_delay90_C.yml b/experiments/OLD_recommendation_delay90_C.yml
similarity index 100%
rename from experiments/recommendation_delay90_C.yml
rename to experiments/OLD_recommendation_delay90_C.yml
diff --git a/experiments/recommendation_delay90_baseline.yml b/experiments/OLD_recommendation_delay90_baseline.yml
similarity index 100%
rename from experiments/recommendation_delay90_baseline.yml
rename to experiments/OLD_recommendation_delay90_baseline.yml
diff --git a/experiments/recommendation_loss15_A.yml b/experiments/OLD_recommendation_loss15_A.yml
similarity index 100%
rename from experiments/recommendation_loss15_A.yml
rename to experiments/OLD_recommendation_loss15_A.yml
diff --git a/experiments/recommendation_loss15_B.yml b/experiments/OLD_recommendation_loss15_B.yml
similarity index 100%
rename from experiments/recommendation_loss15_B.yml
rename to experiments/OLD_recommendation_loss15_B.yml
diff --git a/experiments/recommendation_loss15_C.yml b/experiments/OLD_recommendation_loss15_C.yml
similarity index 100%
rename from experiments/recommendation_loss15_C.yml
rename to experiments/OLD_recommendation_loss15_C.yml
diff --git a/experiments/recommendation_loss15_baseline.yml b/experiments/OLD_recommendation_loss15_baseline.yml
similarity index 100%
rename from experiments/recommendation_loss15_baseline.yml
rename to experiments/OLD_recommendation_loss15_baseline.yml
diff --git a/experiments/recommendation_pause_baseline.yml b/experiments/OLD_recommendation_pause_baseline.yml
similarity index 100%
rename from experiments/recommendation_pause_baseline.yml
rename to experiments/OLD_recommendation_pause_baseline.yml
diff --git a/experiments/test.yml b/experiments/OLD_test.yml
similarity index 98%
rename from experiments/test.yml
rename to experiments/OLD_test.yml
index 537cf75..8c49521 100644
--- a/experiments/test.yml
+++ b/experiments/OLD_test.yml
@@ -177,8 +177,8 @@ experiment:
max_users: 500
spawn_rate: 10
locust_files: [
- { path: locust/locust_basic_interaction.py },
- { path: locust/locust_otel_demo.py },
+ { path: /opt/oxn/locust/locust_basic_interaction.py },
+ { path: /opt/oxn/locust/locust_otel_demo.py },
]
target:
name: astronomy-shop-frontendproxy
diff --git a/experiments/big.yml b/experiments/big.yml
new file mode 100644
index 0000000..c15ed29
--- /dev/null
+++ b/experiments/big.yml
@@ -0,0 +1,180 @@
+# yaml-language-server: $schema=./experiment_schema.json
+experiment:
+ name: big
+ version: 0.0.1
+ orchestrator: kubernetes
+ services:
+ jaeger:
+ name: astronomy-shop-jaeger-query
+ namespace: system-under-evaluation
+ prometheus:
+ [
+ {
+ name: astronomy-shop-prometheus-server,
+ namespace: system-under-evaluation,
+ target: sue,
+ },
+ {
+ name: kube-prometheus-kube-prome-prometheus,
+ namespace: oxn-external-monitoring,
+ target: oxn,
+ },
+ ]
+ responses:
+ - name: frontend_traces
+ type: trace
+ service_name: frontend
+ left_window: 10s
+ right_window: 10s
+ limit: 1
+ - name: system_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation"}[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: recommendation_deployment_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation", pod=~"astronomy-shop-recommendationservice.*"}[90s])) by (pod)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: frontend_http_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_duration_milliseconds_bucket{job="opentelemetry-demo/frontend"}[90s])) by (http_method, http_status_code, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cart_service_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{job="opentelemetry-demo/cartservice"}[90s])) by (http_route, le)) * 1000
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: product_catalog_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(rpc_server_duration_milliseconds_bucket{job="opentelemetry-demo/productcatalogservice"}[90s])) by (rpc_method, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_status
+ type: metric
+ metric_name: sum by (phase) (kube_pod_status_phase{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_restarts
+ type: metric
+ metric_name: sum(kube_pod_container_status_restarts_total{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: terminated_pods
+ type: metric
+ metric_name: sum(kube_pod_container_status_terminated{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: failed_spans
+ type: metric
+ metric_name: sum(rate(otelcol_exporter_send_failed_spans[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_bytes
+ type: metric
+ metric_name: sum(rate(node_network_receive_bytes_total[1m]) + rate(node_network_transmit_bytes_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_drops
+ type: metric
+ metric_name: sum(rate(node_network_receive_drop_total[1m]) + rate(node_network_transmit_drop_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_errors
+ type: metric
+ metric_name: sum(rate(node_network_receive_errs_total[1m]) + rate(node_network_transmit_errs_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_retransmissions
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_RetransSegs[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_udp_errors
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_InErrs[1m]) + rate(node_netstat_Udp_InErrors[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: node_load
+ type: metric
+ metric_name: node_load1
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: memory_available
+ type: metric
+ metric_name: node_memory_MemAvailable_bytes
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cpu_usage
+ type: metric
+ metric_name: sum(rate(node_cpu_seconds_total{mode!="idle"}[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_connections
+ type: metric
+ metric_name: node_netstat_Tcp_CurrEstab
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ treatments:
+ - empty_treatment:
+ action: empty
+ params: { duration: 1m }
+ sue:
+ compose: opentelemetry-demo/docker-compose.yml
+ exclude: [loadgenerator]
+ required:
+ [
+ {
+ namespace: system-under-evaluation,
+ name: astronomy-shop-prometheus-server,
+ },
+ ]
+ loadgen:
+ run_time: 20m
+ max_users: 500
+ spawn_rate: 50
+ locust_files:
+ - "/opt/oxn/locust/locust_basic_interaction.py"
+ - "/opt/oxn/locust/locust_otel_demo.py"
+ target:
+ name: astronomy-shop-frontendproxy
+ namespace: system-under-evaluation
+ port: 8080
diff --git a/experiments/debug_Experiment_packet_loss.yml b/experiments/debug_Experiment_packet_loss.yml
new file mode 100644
index 0000000..85c4a79
--- /dev/null
+++ b/experiments/debug_Experiment_packet_loss.yml
@@ -0,0 +1,48 @@
+# injects a 120s pause in the recomendation service
+experiment:
+ responses:
+ - frontend_traces:
+ type: trace
+ service_name: frontend
+ left_window: 240s
+ right_window: 240s
+ limit: 100000
+ - recommendation_traces:
+ type: trace
+ service_name: recommendationservice
+ left_window: 240s
+ right_window: 240s
+ limit: 100000
+ - system_CPU:
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{container_label_com_docker_compose_project="opentelemetry-demo"}[1m]))
+ left_window: 240s
+ right_window: 240s
+ step: 1
+ - recommendations_total:
+ type: metric
+ metric_name: increase(app_recommendations_counter_total[90s])
+ left_window: 240s
+ right_window: 240s
+ step: 1
+ treatments:
+ - packet_loss_treatment:
+ action: pause
+ params: {
+ service_name: recommendation-service,
+ duration: 30s,
+ }
+ sue:
+ compose: opentelemetry-demo/docker-compose.yml
+ exclude: [loadgenerator]
+ loadgen:
+ run_time: 1m
+ stages:
+ - {duration: 600, users: 50, spawn_rate: 25}
+ tasks:
+ - { endpoint: /, verb: get, weight: 1, params: { } }
+ - { endpoint: /api/products/0PUK6V6EV0, verb: get, weight: 10, params: { } }
+ - { endpoint: /api/recommendations, verb: get, weight: 3, params: { "productIds": ["1YMWWN1N4O"]}}
+ - { endpoint: /api/cart, verb: get, weight: 3, params: { } }
+ - { endpoint: /api/data, verb: get, weight: 3, params: { "contextKeys": [ "accessories" ] } }
+ - { endpoint: /api/cart, verb: post, weight: 2, params: { "item": {"productId":"6E92ZMYYFZ", "quantity":2, }, "userId":'ab2d0fc0-7224-11ec-8ef2-b658b885fb3',} }
\ No newline at end of file
diff --git a/experiments/experiment_new.yml b/experiments/experiment_new.yml
new file mode 100644
index 0000000..91c400b
--- /dev/null
+++ b/experiments/experiment_new.yml
@@ -0,0 +1,227 @@
+# yaml-language-server: $schema=experiment_schema.json
+experiment:
+ name: "latest"
+ version: 0.0.1
+ orchestrator: kubernetes
+ services:
+ jaeger:
+ name: astronomy-shop-jaeger-query
+ namespace: system-under-evaluation
+ prometheus:
+ [
+ {
+ name: astronomy-shop-prometheus-server,
+ namespace: system-under-evaluation,
+ target: sue,
+ },
+ {
+ name: kube-prometheus-kube-prome-prometheus,
+ namespace: oxn-external-monitoring,
+ target: oxn,
+ },
+ ]
+ responses:
+ - name: frontend_traces
+ type: trace
+ service_name: frontend
+ left_window: 10s
+ right_window: 10s
+ limit: 1
+ - name: system_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation"}[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: recommendation_deployment_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation", pod=~"astronomy-shop-recommendationservice.*"}[90s])) by (pod)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: frontend_http_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_duration_milliseconds_bucket{job="opentelemetry-demo/frontend"}[90s])) by (http_method, http_status_code, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cart_service_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{job="opentelemetry-demo/cartservice"}[90s])) by (http_route, le)) * 1000
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: product_catalog_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(rpc_server_duration_milliseconds_bucket{job="opentelemetry-demo/productcatalogservice"}[90s])) by (rpc_method, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_status
+ type: metric
+ metric_name: sum by (phase) (kube_pod_status_phase{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_restarts
+ type: metric
+ metric_name: sum(kube_pod_container_status_restarts_total{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: terminated_pods
+ type: metric
+ metric_name: sum(kube_pod_container_status_terminated{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: failed_spans
+ type: metric
+ metric_name: sum(rate(otelcol_exporter_send_failed_spans[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_bytes
+ type: metric
+ metric_name: sum(rate(node_network_receive_bytes_total[1m]) + rate(node_network_transmit_bytes_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_drops
+ type: metric
+ metric_name: sum(rate(node_network_receive_drop_total[1m]) + rate(node_network_transmit_drop_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_errors
+ type: metric
+ metric_name: sum(rate(node_network_receive_errs_total[1m]) + rate(node_network_transmit_errs_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_retransmissions
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_RetransSegs[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_udp_errors
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_InErrs[1m]) + rate(node_netstat_Udp_InErrors[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: node_load
+ type: metric
+ metric_name: node_load1
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: memory_available
+ type: metric
+ metric_name: node_memory_MemAvailable_bytes
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cpu_usage
+ type: metric
+ metric_name: sum(rate(node_cpu_seconds_total{mode!="idle"}[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_connections
+ type: metric
+ metric_name: node_netstat_Tcp_CurrEstab
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ treatments:
+ - add_security_context:
+ action: security_context_kubernetes
+ params:
+ {
+ namespace: system-under-evaluation,
+ label_selector: app.kubernetes.io/component,
+ label: recommendationservice,
+ capabilities: { add: ["NET_ADMIN"] },
+ }
+ - delay_treatment:
+ action: delay
+ params: {
+ namespace: system-under-evaluation,
+ label_selector: app.kubernetes.io/name,
+ label: astronomy-shop-recommendationservice,
+ #service_name: node-exporter,
+ delay_time: 45ms,
+ delay_jitter: 45ms,
+ duration: 2m,
+ interface: eth0,
+ }
+ - probabilistic_head_sampling_rate:
+ action: kube_probl
+ params: { sampling_percentage: 5.0, hash_seed: 22 }
+ - package_lost_treatment:
+ action: kubernetes_loss
+ params:
+ {
+ namespace: system-under-evaluation,
+ label_selector: app.kubernetes.io/name,
+ label: astronomy-shop-recommendationservice,
+ loss_percentage: 15.0,
+ duration: 1m,
+ interface: eth0,
+ }
+ - empty_treatment:
+ action: empty
+ params: { duration: 5m }
+ - increase_otel_metric_interval:
+ action: kubernetes_otel_metrics_interval
+ params:
+ {
+ namespace: system-under-evaluation,
+ label_selector: app.kubernetes.io/component,
+ label: recommendationservice,
+ interval: 15s,
+ }
+ - prometheus_scrape_interval:
+ action: kubernetes_prometheus_interval
+ params: { interval: 5s, evaluation_interval: 5s, scrape_timeout: 3s }
+ sue:
+ compose: opentelemetry-demo/docker-compose.yml
+ exclude: [loadgenerator]
+ required: [
+ {
+ namespace: system-under-evaluation,
+ name: astronomy-shop-prometheus-server,
+ },
+ ] # {namespace: monitoring, name: not-running-service}
+ #required: [{namespace: monitoring, name: grafana}, {namespace: monitoring, name: node-exporter}]
+ loadgen:
+ run_time: 5m
+ max_users: 50
+ spawn_rate: 10
+ locust_files:
+ - "/opt/oxn/locust/locust_basic_interaction.py"
+ - "/opt/oxn/locust/locust_otel_demo.py"
+ target:
+ name: astronomy-shop-frontendproxy
+ namespace: system-under-evaluation
+ port: 8080
diff --git a/experiments/experiment_schema.json b/experiments/experiment_schema.json
new file mode 100644
index 0000000..ec33296
--- /dev/null
+++ b/experiments/experiment_schema.json
@@ -0,0 +1,255 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "experiment": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "orchestrator": {
+ "type": "string"
+ },
+ "services": {
+ "type": "object",
+ "properties": {
+ "jaeger": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "namespace"
+ ]
+ },
+ "prometheus": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "namespace",
+ "target"
+ ]
+ }
+ }
+ },
+ "required": [
+ "jaeger",
+ "prometheus"
+ ]
+ },
+ "responses": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ },
+ "metric_name": {
+ "type": "string"
+ },
+ "type": {
+ "const": "metric"
+ },
+ "step": {
+ "type": "integer"
+ },
+ "left_window": {
+ "type": "string"
+ },
+ "right_window": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "target",
+ "metric_name",
+ "type",
+ "step",
+ "left_window",
+ "right_window"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "const": "trace"
+ },
+ "service_name": {
+ "type": "string"
+ },
+ "left_window": {
+ "type": "string"
+ },
+ "right_window": {
+ "type": "string"
+ },
+ "limit": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "name",
+ "type",
+ "service_name",
+ "left_window",
+ "right_window"
+ ]
+ }
+ ]
+ }
+ },
+ "treatments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "params": {
+ "type": "object"
+ }
+ },
+ "required": [
+ "action",
+ "params"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "sue": {
+ "type": "object",
+ "properties": {
+ "compose": {
+ "type": "string"
+ },
+ "exclude": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "include": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "required": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "namespace": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "namespace",
+ "name"
+ ]
+ }
+ }
+ },
+ "required": [
+ "compose"
+ ]
+ },
+ "loadgen": {
+ "type": "object",
+ "properties": {
+ "run_time": {
+ "type": "string"
+ },
+ "max_users": {
+ "type": "integer"
+ },
+ "spawn_rate": {
+ "type": "integer"
+ },
+ "locust_files": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "target": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "name",
+ "namespace",
+ "port"
+ ]
+ }
+ },
+ "required": [
+ "run_time"
+ ]
+ }
+ },
+ "required": [
+ "version",
+ "orchestrator",
+ "responses",
+ "sue",
+ "loadgen"
+ ]
+ }
+ },
+ "required": [
+ "experiment"
+ ]
+}
\ No newline at end of file
diff --git a/experiments/latest.yml b/experiments/latest.yml
new file mode 100644
index 0000000..8594a3f
--- /dev/null
+++ b/experiments/latest.yml
@@ -0,0 +1,245 @@
+# yaml-language-server: $schema=experiment_schema.json
+experiment:
+ name: "latest"
+ version: 0.0.1
+ orchestrator: kubernetes
+ services:
+ jaeger:
+ name: astronomy-shop-jaeger-query
+ namespace: system-under-evaluation
+ prometheus:
+ [
+ {
+ name: astronomy-shop-prometheus-server,
+ namespace: system-under-evaluation,
+ target: sue,
+ },
+ {
+ name: kube-prometheus-kube-prome-prometheus,
+ namespace: oxn-external-monitoring,
+ target: oxn,
+ },
+ ]
+ responses:
+ - name: frontend_traces
+ type: trace
+ service_name: frontend
+ left_window: 10s
+ right_window: 10s
+ limit: 1
+ - name: system_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation"}[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: recommendation_deployment_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation", pod=~"astronomy-shop-recommendationservice.*"}[90s])) by (pod)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: frontend_http_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_duration_milliseconds_bucket{job="opentelemetry-demo/frontend"}[90s])) by (http_method, http_status_code, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cart_service_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{job="opentelemetry-demo/cartservice"}[90s])) by (http_route, le)) * 1000
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: product_catalog_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(rpc_server_duration_milliseconds_bucket{job="opentelemetry-demo/productcatalogservice"}[90s])) by (rpc_method, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_status
+ type: metric
+ metric_name: sum by (phase) (kube_pod_status_phase{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_restarts
+ type: metric
+ metric_name: sum(kube_pod_container_status_restarts_total{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: terminated_pods
+ type: metric
+ metric_name: sum(kube_pod_container_status_terminated{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: failed_spans
+ type: metric
+ metric_name: sum(rate(otelcol_exporter_send_failed_spans[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_bytes
+ type: metric
+ metric_name: sum(rate(node_network_receive_bytes_total[1m]) + rate(node_network_transmit_bytes_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_drops
+ type: metric
+ metric_name: sum(rate(node_network_receive_drop_total[1m]) + rate(node_network_transmit_drop_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_errors
+ type: metric
+ metric_name: sum(rate(node_network_receive_errs_total[1m]) + rate(node_network_transmit_errs_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_retransmissions
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_RetransSegs[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_udp_errors
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_InErrs[1m]) + rate(node_netstat_Udp_InErrors[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: node_load
+ type: metric
+ metric_name: node_load1
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: memory_available
+ type: metric
+ metric_name: node_memory_MemAvailable_bytes
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cpu_usage
+ type: metric
+ metric_name: sum(rate(node_cpu_seconds_total{mode!="idle"}[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_connections
+ type: metric
+ metric_name: node_netstat_Tcp_CurrEstab
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ treatments:
+ - empty_treatment:
+ action: empty
+ params: { duration: 5m }
+ #- stop_loadgen_deployment:
+ # action: scale_deployment
+ # params: {
+ # namespace: system-under-evaluation,
+ # label_selector: app.kubernetes.io/component,
+ # label: loadgenerator,
+ # scale_to: 0,
+ # }
+ #- add_security_context:
+ # action: security_context_kubernetes
+ # params: {
+ # namespace: system-under-evaluation,
+ # label_selector: app.kubernetes.io/component,
+ # label: recommendationservice,
+ # capabilities: { add: ["NET_ADMIN"] },
+ # }
+ # - delay_treatment:
+ # action: delay
+ # params: {
+ # namespace: system-under-evaluation,
+ # label_selector: app.kubernetes.io/name,
+ # label: astronomy-shop-recommendationservice,
+ # #service_name: node-exporter,
+ # delay_time: 45ms,
+ # delay_jitter: 45ms,
+ # duration: 2m,
+ # interface: eth0,
+ # }
+ # - probabilistic_head_sampling_rate:
+ # action: kube_probl
+ # params: {
+ # sampling_percentage: 5.0,
+ # hash_seed: 22,
+ # }
+ #- package_lost_treatment:
+ # action: kubernetes_loss
+ # params: {
+ # namespace: system-under-evaluation,
+ # label_selector: app.kubernetes.io/name,
+ # label: astronomy-shop-recommendationservice,
+ # loss_percentage: 15.0,
+ # duration: 10m,
+ # interface: eth0,
+ # }
+ #- empty_treatment:
+ # action: empty
+ # params: {
+ # duration: 5m,
+ # }
+ #- increase_otel_metric_interval:
+ # action: kubernetes_otel_metrics_interval
+ # params: {
+ # namespace: system-under-evaluation,
+ # label_selector: app.kubernetes.io/component,
+ # label: recommendationservice,
+ # interval: 15s,
+ # }
+ #reloading the prometheus config is not as trivial as it seems to be
+ #- prometheus_scrape_interval:
+ # action: kubernetes_prometheus_interval
+ # params: {
+ # interval: 5s,
+ # evaluation_interval: 5s,
+ # scrape_timeout: 3s,
+ # }
+ sue:
+ compose: opentelemetry-demo/docker-compose.yml
+ exclude: [loadgenerator]
+ required: [
+ {
+ namespace: system-under-evaluation,
+ name: astronomy-shop-prometheus-server,
+ },
+ ] # {namespace: monitoring, name: not-running-service}
+ #required: [{namespace: monitoring, name: grafana}, {namespace: monitoring, name: node-exporter}]
+ loadgen:
+ run_time: 5m
+ max_users: 50
+ spawn_rate: 10
+ locust_files:
+ - "/opt/oxn/locust/locust_basic_interaction.py"
+ - "/opt/oxn/locust/locust_otel_demo.py"
+ target:
+ name: astronomy-shop-frontendproxy
+ namespace: system-under-evaluation
+ port: 8080
diff --git a/experiments/short.yml b/experiments/short.yml
new file mode 100644
index 0000000..ff40bc9
--- /dev/null
+++ b/experiments/short.yml
@@ -0,0 +1,180 @@
+# yaml-language-server: $schema=experiment_schema.json
+experiment:
+ name: k8s-test-successful
+ version: 0.0.1
+ orchestrator: kubernetes
+ services:
+ jaeger:
+ name: astronomy-shop-jaeger-query
+ namespace: system-under-evaluation
+ prometheus:
+ [
+ {
+ name: astronomy-shop-prometheus-server,
+ namespace: system-under-evaluation,
+ target: sue,
+ },
+ {
+ name: kube-prometheus-kube-prome-prometheus,
+ namespace: oxn-external-monitoring,
+ target: oxn,
+ },
+ ]
+ responses:
+ - name: frontend_traces
+ type: trace
+ service_name: frontend
+ left_window: 10s
+ right_window: 10s
+ limit: 1
+ - name: system_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation"}[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: recommendation_deployment_CPU
+ type: metric
+ metric_name: sum(rate(container_cpu_usage_seconds_total{namespace="system-under-evaluation", pod=~"astronomy-shop-recommendationservice.*"}[90s])) by (pod)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: oxn
+ - name: frontend_http_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_duration_milliseconds_bucket{job="opentelemetry-demo/frontend"}[90s])) by (http_method, http_status_code, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cart_service_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{job="opentelemetry-demo/cartservice"}[90s])) by (http_route, le)) * 1000
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: product_catalog_latency
+ type: metric
+ metric_name: histogram_quantile(0.95, sum(rate(rpc_server_duration_milliseconds_bucket{job="opentelemetry-demo/productcatalogservice"}[90s])) by (rpc_method, le))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_status
+ type: metric
+ metric_name: sum by (phase) (kube_pod_status_phase{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: pod_restarts
+ type: metric
+ metric_name: sum(kube_pod_container_status_restarts_total{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: terminated_pods
+ type: metric
+ metric_name: sum(kube_pod_container_status_terminated{namespace="system-under-evaluation"})
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: failed_spans
+ type: metric
+ metric_name: sum(rate(otelcol_exporter_send_failed_spans[1m]))
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_bytes
+ type: metric
+ metric_name: sum(rate(node_network_receive_bytes_total[1m]) + rate(node_network_transmit_bytes_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_drops
+ type: metric
+ metric_name: sum(rate(node_network_receive_drop_total[1m]) + rate(node_network_transmit_drop_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: network_errors
+ type: metric
+ metric_name: sum(rate(node_network_receive_errs_total[1m]) + rate(node_network_transmit_errs_total[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_retransmissions
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_RetransSegs[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_udp_errors
+ type: metric
+ metric_name: sum(rate(node_netstat_Tcp_InErrs[1m]) + rate(node_netstat_Udp_InErrors[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: node_load
+ type: metric
+ metric_name: node_load1
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: memory_available
+ type: metric
+ metric_name: node_memory_MemAvailable_bytes
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: cpu_usage
+ type: metric
+ metric_name: sum(rate(node_cpu_seconds_total{mode!="idle"}[1m])) by (instance)
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ - name: tcp_connections
+ type: metric
+ metric_name: node_netstat_Tcp_CurrEstab
+ left_window: 10s
+ right_window: 10s
+ step: 1
+ target: sue
+ treatments:
+ - empty_treatment:
+ action: empty
+ params: { duration: 1m }
+ sue:
+ compose: opentelemetry-demo/docker-compose.yml
+ exclude: [loadgenerator]
+ required:
+ [
+ {
+ namespace: system-under-evaluation,
+ name: astronomy-shop-prometheus-server,
+ },
+ ]
+ loadgen:
+ run_time: 2m
+ max_users: 10
+ spawn_rate: 5
+ locust_files:
+ - "/opt/oxn/locust/locust_basic_interaction.py"
+ - "/opt/oxn/locust/locust_otel_demo.py"
+ target:
+ name: astronomy-shop-frontendproxy
+ namespace: system-under-evaluation
+ port: 8080
diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
new file mode 100644
index 0000000..3722418
--- /dev/null
+++ b/frontend/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["next/core-web-vitals", "next/typescript"]
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..fd3dbb5
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..d7bd896
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,66 @@
+# syntax=docker.io/docker/dockerfile:1
+
+FROM node:18-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
+RUN \
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
+ elif [ -f package-lock.json ]; then npm ci; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+
+# Rebuild the source code only when needed
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Next.js collects completely anonymous telemetry data about general usage.
+# Learn more here: https://nextjs.org/telemetry
+# Uncomment the following line in case you want to disable telemetry during the build.
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN \
+ if [ -f yarn.lock ]; then yarn run build; \
+ elif [ -f package-lock.json ]; then npm run build; \
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+# Production image, copy all the files and run next
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+# Uncomment the following line in case you want to disable telemetry during runtime.
+# ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT=3000
+
+# server.js is created by next build from the standalone output
+# https://nextjs.org/docs/pages/api-reference/next-config-js/output
+ENV HOSTNAME="0.0.0.0"
+CMD ["node", "server.js"]
\ No newline at end of file
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..6a31951
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,15 @@
+## Getting Started
+
+First, install dependencies:
+
+```bash
+npm install
+```
+
+Then, run the development server:
+
+```bash
+npm run dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
diff --git a/frontend/app/error.tsx b/frontend/app/error.tsx
new file mode 100644
index 0000000..5521b61
--- /dev/null
+++ b/frontend/app/error.tsx
@@ -0,0 +1,18 @@
+'use client'
+import { Button } from "@/components/ui/button";
+
+export default function ErrorPage({ error, reset }: { error: Error; reset: () => void }) {
+
+ return (
+
+
Something went wrong
+
{error?.message || 'An unexpected error has occurred.'}
+
reset()}
+ className="px-4 py-2"
+ >
+ Try Again
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/experiment-setup/page.tsx b/frontend/app/experiment-setup/page.tsx
new file mode 100644
index 0000000..4f071eb
--- /dev/null
+++ b/frontend/app/experiment-setup/page.tsx
@@ -0,0 +1,8 @@
+export default function ExperimentSetupPage() {
+
+ return (
+
+ All the experiments setups will be included here...
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/frontend/app/favicon.ico differ
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
new file mode 100644
index 0000000..57e6550
--- /dev/null
+++ b/frontend/app/layout.tsx
@@ -0,0 +1,36 @@
+import type { Metadata } from "next";
+import { ThemeProvider } from "@/context/theme-provider";
+import "@/styles/globals.css"
+import Layout from "@/components/layout";
+
+export const metadata: Metadata = {
+ title: "OXN++ Dashboard",
+ description: "A user-friendly interface for configuring, monitoring, and analyzing observability experiments in cloud-native applications.",
+ keywords: ["OXN", "Observability", "Microservices", "Dashboard", "Cloud-Native", "Fault Injection", "Performance Monitoring"],
+ authors: [],
+ applicationName: "OXN++",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx
new file mode 100644
index 0000000..1ac0ca0
--- /dev/null
+++ b/frontend/app/not-found.tsx
@@ -0,0 +1,16 @@
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+
+export default function NotFound() {
+ return (
+
+
404 - Page Not Found
+
The page you are looking for does not exist or has been moved.
+
+
+ Go Back Home
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000..6a6591c
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,51 @@
+'use client'
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ DialogFooter
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+
+export default function Home() {
+
+ return (
+
+
+
+ Start experiment
+
+
+
+ Start new experiment
+
+ Upload a YAML file with experiment configurations.
+
+
+
+
+ Save changes
+
+
+
+
+ );
+}
diff --git a/frontend/app/public/READ.MD b/frontend/app/public/READ.MD
new file mode 100644
index 0000000..bc62aa5
--- /dev/null
+++ b/frontend/app/public/READ.MD
@@ -0,0 +1 @@
+This dir is for all static assets used by the frontend.
\ No newline at end of file
diff --git a/frontend/app/real-time/page.tsx b/frontend/app/real-time/page.tsx
new file mode 100644
index 0000000..d994322
--- /dev/null
+++ b/frontend/app/real-time/page.tsx
@@ -0,0 +1,8 @@
+export default function RealTimeMonitoring() {
+
+ return (
+
+ Real time monitoring coming soon....
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/results-and-reports/page.tsx b/frontend/app/results-and-reports/page.tsx
new file mode 100644
index 0000000..583118d
--- /dev/null
+++ b/frontend/app/results-and-reports/page.tsx
@@ -0,0 +1,8 @@
+export default function ResultsAndReportsPage() {
+
+ return (
+
+ All the reports and results will be included here...
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx
new file mode 100644
index 0000000..629713e
--- /dev/null
+++ b/frontend/app/search/page.tsx
@@ -0,0 +1,8 @@
+export default function SearchPage() {
+
+ return (
+
+ Global search page...
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx
new file mode 100644
index 0000000..15aa2ef
--- /dev/null
+++ b/frontend/app/settings/page.tsx
@@ -0,0 +1,8 @@
+export default function SettingsPage() {
+
+ return (
+
+ Global settings page...
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 0000000..c8c85e6
--- /dev/null
+++ b/frontend/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "styles/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx
new file mode 100644
index 0000000..bc4bcb7
--- /dev/null
+++ b/frontend/components/app-sidebar.tsx
@@ -0,0 +1,44 @@
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+import { items } from "@/configurations/menu"
+
+export function AppSidebar() {
+
+ return (
+
+
+
+
+
+ MAIN
+
+
+ {items.map((item) => (
+
+
+
+
+ {item.title}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/components/dark-mode-toggle.tsx b/frontend/components/dark-mode-toggle.tsx
new file mode 100644
index 0000000..a92b982
--- /dev/null
+++ b/frontend/components/dark-mode-toggle.tsx
@@ -0,0 +1,42 @@
+"use client"
+
+import * as React from "react"
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "next-themes"
+
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+
+export function ThemeToggle() {
+ const { setTheme } = useTheme()
+
+ return (
+
+
+
+
+
+
+ Toggle theme
+
+
+
+ setTheme("light")}>
+ Light
+
+ setTheme("dark")}>
+ Dark
+
+ setTheme("system")}>
+ System
+
+
+
+
+ )
+}
diff --git a/frontend/components/layout.tsx b/frontend/components/layout.tsx
new file mode 100644
index 0000000..b62e37d
--- /dev/null
+++ b/frontend/components/layout.tsx
@@ -0,0 +1,20 @@
+import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
+import { AppSidebar } from "./app-sidebar"
+import { ThemeToggle } from "./dark-mode-toggle"
+export default function Layout({ children }: { children: React.ReactNode }) {
+
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
new file mode 100644
index 0000000..65d4fcd
--- /dev/null
+++ b/frontend/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx
new file mode 100644
index 0000000..1647513
--- /dev/null
+++ b/frontend/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..082639f
--- /dev/null
+++ b/frontend/components/ui/dropdown-menu.tsx
@@ -0,0 +1,201 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx
new file mode 100644
index 0000000..69b64fb
--- /dev/null
+++ b/frontend/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx
new file mode 100644
index 0000000..5341821
--- /dev/null
+++ b/frontend/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/frontend/components/ui/separator.tsx b/frontend/components/ui/separator.tsx
new file mode 100644
index 0000000..12d81c4
--- /dev/null
+++ b/frontend/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/frontend/components/ui/sheet.tsx b/frontend/components/ui/sheet.tsx
new file mode 100644
index 0000000..272cb72
--- /dev/null
+++ b/frontend/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+
+
+ Close
+
+ {children}
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/frontend/components/ui/sidebar.tsx b/frontend/components/ui/sidebar.tsx
new file mode 100644
index 0000000..eeb2d7a
--- /dev/null
+++ b/frontend/components/ui/sidebar.tsx
@@ -0,0 +1,763 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { PanelLeft } from "lucide-react"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Separator } from "@/components/ui/separator"
+import { Sheet, SheetContent } from "@/components/ui/sheet"
+import { Skeleton } from "@/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar:state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContext = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+ }
+)
+SidebarProvider.displayName = "SidebarProvider"
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+ }
+)
+Sidebar.displayName = "Sidebar"
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+})
+SidebarTrigger.displayName = "SidebarTrigger"
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarRail.displayName = "SidebarRail"
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInset.displayName = "SidebarInset"
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInput.displayName = "SidebarInput"
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarHeader.displayName = "SidebarHeader"
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarFooter.displayName = "SidebarFooter"
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarSeparator.displayName = "SidebarSeparator"
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarContent.displayName = "SidebarContent"
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarGroup.displayName = "SidebarGroup"
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupLabel.displayName = "SidebarGroupLabel"
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupAction.displayName = "SidebarGroupAction"
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarGroupContent.displayName = "SidebarGroupContent"
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenu.displayName = "SidebarMenu"
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuItem.displayName = "SidebarMenuItem"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+ }
+)
+SidebarMenuButton.displayName = "SidebarMenuButton"
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuAction.displayName = "SidebarMenuAction"
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuBadge.displayName = "SidebarMenuBadge"
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+})
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuSub.displayName = "SidebarMenuSub"
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => )
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx
new file mode 100644
index 0000000..d7e45f7
--- /dev/null
+++ b/frontend/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/frontend/components/ui/tooltip.tsx b/frontend/components/ui/tooltip.tsx
new file mode 100644
index 0000000..a66b3f2
--- /dev/null
+++ b/frontend/components/ui/tooltip.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/frontend/configurations/menu.ts b/frontend/configurations/menu.ts
new file mode 100644
index 0000000..7b7589c
--- /dev/null
+++ b/frontend/configurations/menu.ts
@@ -0,0 +1,41 @@
+import { MenuId } from "@/types"
+import { FileChartLine, Search, Settings, Gauge, FileSliders, Activity } from "lucide-react"
+
+export const items = [
+ {
+ id: MenuId.DASHBOARD,
+ title: "Dashboard",
+ url: "/",
+ icon: Gauge,
+ },
+ {
+ id: MenuId.RESULTS,
+ title: "Results & reports",
+ url: "/results-and-reports",
+ icon: FileChartLine,
+ },
+ {
+ id: MenuId.EXPERIMENT,
+ title: "Experiment setup",
+ url: "/experiment-setup",
+ icon: FileSliders,
+ },
+ {
+ id: MenuId.REALTIME,
+ title: 'Real-time monitoring',
+ url: '/real-time',
+ icon: Activity,
+ },
+ {
+ id: MenuId.SEARCH,
+ title: "Search",
+ url: "/search",
+ icon: Search,
+ },
+ {
+ id: MenuId.SETTINGS,
+ title: "Settings",
+ url: "/settings",
+ icon: Settings,
+ }
+]
\ No newline at end of file
diff --git a/frontend/context/theme-provider.tsx b/frontend/context/theme-provider.tsx
new file mode 100644
index 0000000..6a1ffe4
--- /dev/null
+++ b/frontend/context/theme-provider.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as React from "react"
+import { ThemeProvider as NextThemesProvider } from "next-themes"
+
+export function ThemeProvider({
+ children,
+ ...props
+}: React.ComponentProps) {
+ return {children}
+}
diff --git a/frontend/hooks/use-mobile.tsx b/frontend/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/frontend/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/frontend/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
new file mode 100644
index 0000000..c289099
--- /dev/null
+++ b/frontend/next.config.mjs
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: 'standalone', // Aktiviert die Standalone-Option
+ };
+
+ export default nextConfig;
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..aaf5346
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,5662 @@
+{
+ "name": "oxn-view",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "oxn-view",
+ "version": "0.1.0",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-separator": "^1.1.0",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.4",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.456.0",
+ "next": "14.2.16",
+ "next-themes": "^0.4.3",
+ "oxn-view": "file:",
+ "react": "^18",
+ "react-dom": "^18",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint": "^8",
+ "eslint-config-next": "14.2.16",
+ "postcss": "^8",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
+ "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.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"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+ "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+ "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.16.tgz",
+ "integrity": "sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag=="
+ },
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.16.tgz",
+ "integrity": "sha512-noORwKUMkKc96MWjTOwrsUCjky0oFegHbeJ1yEnQBGbMHAaTEIgLZIIfsYF0x3a06PiS+2TXppfifR+O6VWslg==",
+ "dev": true,
+ "dependencies": {
+ "glob": "10.3.10"
+ }
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.16.tgz",
+ "integrity": "sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.16.tgz",
+ "integrity": "sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.16.tgz",
+ "integrity": "sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.16.tgz",
+ "integrity": "sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.16.tgz",
+ "integrity": "sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.16.tgz",
+ "integrity": "sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.16.tgz",
+ "integrity": "sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz",
+ "integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.16.tgz",
+ "integrity": "sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nolyfill/is-core-module": {
+ "version": "1.0.39",
+ "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
+ "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.4.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
+ "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
+ "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
+ "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz",
+ "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
+ "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz",
+ "integrity": "sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-menu": "2.1.2",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+ "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
+ "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+ "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz",
+ "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
+ "integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-roving-focus": "1.1.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
+ "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
+ "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
+ "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
+ "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
+ "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",
+ "integrity": "sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+ "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+ "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
+ "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true
+ },
+ "node_modules/@rushstack/eslint-patch": {
+ "version": "1.10.4",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz",
+ "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
+ "dev": true
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "20.17.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz",
+ "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.13",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
+ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
+ "devOptional": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.12",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
+ "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz",
+ "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.14.0",
+ "@typescript-eslint/type-utils": "8.14.0",
+ "@typescript-eslint/utils": "8.14.0",
+ "@typescript-eslint/visitor-keys": "8.14.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz",
+ "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.14.0",
+ "@typescript-eslint/types": "8.14.0",
+ "@typescript-eslint/typescript-estree": "8.14.0",
+ "@typescript-eslint/visitor-keys": "8.14.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz",
+ "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.14.0",
+ "@typescript-eslint/visitor-keys": "8.14.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz",
+ "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.14.0",
+ "@typescript-eslint/utils": "8.14.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz",
+ "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz",
+ "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.14.0",
+ "@typescript-eslint/visitor-keys": "8.14.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": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz",
+ "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.14.0",
+ "@typescript-eslint/types": "8.14.0",
+ "@typescript-eslint/typescript-estree": "8.14.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz",
+ "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.14.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
+ "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+ "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+ "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
+ "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+ "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+ "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+ "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.2.1",
+ "get-intrinsic": "^1.2.3",
+ "is-array-buffer": "^3.0.4",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.10.2",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz",
+ "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001680",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz",
+ "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz",
+ "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==",
+ "dependencies": {
+ "clsx": "2.0.0"
+ },
+ "funding": {
+ "url": "https://joebell.co.uk"
+ }
+ },
+ "node_modules/class-variance-authority/node_modules/clsx": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
+ "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "devOptional": true
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
+ "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
+ "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.17.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
+ "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.23.4",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.4.tgz",
+ "integrity": "sha512-HR1gxH5OaiN7XH7uiWH0RLw0RcFySiSoW1ctxmD1ahTw3uGBtkmm/ng0tDU1OtYx5OK6EOL5Y6O21cDflG3Jcg==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "arraybuffer.prototype.slice": "^1.0.3",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "data-view-buffer": "^1.0.1",
+ "data-view-byte-length": "^1.0.1",
+ "data-view-byte-offset": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-set-tostringtag": "^2.0.3",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.4",
+ "get-symbol-description": "^1.0.2",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.3",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.0.7",
+ "is-array-buffer": "^3.0.4",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.1",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.3",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.13",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.13.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.5",
+ "regexp.prototype.flags": "^1.5.3",
+ "safe-array-concat": "^1.1.2",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.trim": "^1.2.9",
+ "string.prototype.trimend": "^1.0.8",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.2",
+ "typed-array-byte-length": "^1.0.1",
+ "typed-array-byte-offset": "^1.0.2",
+ "typed-array-length": "^1.0.6",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.15"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz",
+ "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.3",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.7",
+ "iterator.prototype": "^1.1.3",
+ "safe-array-concat": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+ "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-next": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.16.tgz",
+ "integrity": "sha512-HOcnCJsyLXR7B8wmjaCgkTSpz+ijgOyAkP8OlvANvciP8PspBYFEBTmakNMxOf71fY0aKOm/blFIiKnrM4K03Q==",
+ "dev": true,
+ "dependencies": {
+ "@next/eslint-plugin-next": "14.2.16",
+ "@rushstack/eslint-patch": "^1.3.3",
+ "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^3.5.2",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
+ },
+ "peerDependencies": {
+ "eslint": "^7.23.0 || ^8.0.0",
+ "typescript": ">=3.3.1"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.6.3",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz",
+ "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==",
+ "dev": true,
+ "dependencies": {
+ "@nolyfill/is-core-module": "1.0.39",
+ "debug": "^4.3.5",
+ "enhanced-resolve": "^5.15.0",
+ "eslint-module-utils": "^2.8.1",
+ "fast-glob": "^3.3.2",
+ "get-tsconfig": "^4.7.5",
+ "is-bun-module": "^1.0.2",
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*",
+ "eslint-plugin-import-x": "*"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-import": {
+ "optional": true
+ },
+ "eslint-plugin-import-x": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
+ "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.31.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
+ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
+ "dev": true,
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.8",
+ "array.prototype.findlastindex": "^1.2.5",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.0",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.15.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.0",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.8",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+ "dev": true,
+ "dependencies": {
+ "aria-query": "^5.3.2",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz",
+ "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.2",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.1.0",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.8",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.0",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.11",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.0.0-canary-7118f5dd7-20230705",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz",
+ "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "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.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+ "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+ "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
+ "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.3.10",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+ "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.5",
+ "minimatch": "^9.0.1",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+ "path-scurry": "^1.10.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+ "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+ "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+ "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bun-module": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz",
+ "integrity": "sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.6.3"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
+ "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
+ "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+ "dev": true,
+ "dependencies": {
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+ "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+ "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
+ "dev": true,
+ "dependencies": {
+ "which-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
+ "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz",
+ "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "reflect.getprototypeof": "^1.0.4",
+ "set-function-name": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+ "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.6",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+ "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/lucide-react": {
+ "version": "0.456.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.456.0.tgz",
+ "integrity": "sha512-DIIGJqTT5X05sbAsQ+OhA8OtJYyD4NsEMCA/HQW/Y6ToPQ7gwbtujIoeAaup4HpHzV35SQOarKAWH8LYglB6eA==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/next": {
+ "version": "14.2.16",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.16.tgz",
+ "integrity": "sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==",
+ "dependencies": {
+ "@next/env": "14.2.16",
+ "@swc/helpers": "0.5.5",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001579",
+ "graceful-fs": "^4.2.11",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.1"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=18.17.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "14.2.16",
+ "@next/swc-darwin-x64": "14.2.16",
+ "@next/swc-linux-arm64-gnu": "14.2.16",
+ "@next/swc-linux-arm64-musl": "14.2.16",
+ "@next/swc-linux-x64-gnu": "14.2.16",
+ "@next/swc-linux-x64-musl": "14.2.16",
+ "@next/swc-win32-arm64-msvc": "14.2.16",
+ "@next/swc-win32-ia32-msvc": "14.2.16",
+ "@next/swc-win32-x64-msvc": "14.2.16"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.41.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next-themes": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.3.tgz",
+ "integrity": "sha512-nG84VPkTdUHR2YeD89YchvV4I9RbiMAql3GiLEQlPvq1ioaqPaIReK+yMRdg/zgiXws620qS1rU30TiWmmG9lA==",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/next/node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
+ "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
+ "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "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"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/oxn-view": {
+ "resolved": "",
+ "link": true
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-load-config/node_modules/lilconfig": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
+ "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dev": true,
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
+ "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.6",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
+ "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
+ "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.1",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "globalthis": "^1.0.3",
+ "which-builtin-type": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
+ "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+ "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+ "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.1.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/string.prototype.includes": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
+ "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
+ "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.7",
+ "regexp.prototype.flags": "^1.5.2",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
+ "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
+ "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
+ "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.14",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
+ "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.5.3",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.0",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.0",
+ "lilconfig": "^2.1.0",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.0.0",
+ "postcss": "^8.4.23",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.1",
+ "postcss-nested": "^6.0.1",
+ "postcss-selector-parser": "^6.0.11",
+ "resolve": "^1.22.2",
+ "sucrase": "^3.32.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz",
+ "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+ "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+ "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
+ "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "dev": true
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
+ "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz",
+ "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==",
+ "dev": true,
+ "dependencies": {
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.0.5",
+ "is-finalizationregistry": "^1.0.2",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.1.4",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.15"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+ "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/yaml": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
+ "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..678cd38
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "oxn-view",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-separator": "^1.1.0",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.4",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.456.0",
+ "next": "14.2.16",
+ "next-themes": "^0.4.3",
+ "oxn-view": "file:",
+ "react": "^18",
+ "react-dom": "^18",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint": "^8",
+ "eslint-config-next": "14.2.16",
+ "postcss": "^8",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5"
+ }
+}
diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs
new file mode 100644
index 0000000..1a69fd2
--- /dev/null
+++ b/frontend/postcss.config.mjs
@@ -0,0 +1,8 @@
+/** @type {import('postcss-load-config').Config} */
+const config = {
+ plugins: {
+ tailwindcss: {},
+ },
+};
+
+export default config;
diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css
new file mode 100644
index 0000000..2847583
--- /dev/null
+++ b/frontend/styles/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary: 240 5.9% 10%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 240 10% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+ .dark {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 240 5.9% 10%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 240 4.9% 83.9%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
new file mode 100644
index 0000000..2d83a87
--- /dev/null
+++ b/frontend/tailwind.config.ts
@@ -0,0 +1,73 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ darkMode: ["class"],
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))'
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))'
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))'
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))'
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))'
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))'
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))'
+ },
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ chart: {
+ '1': 'hsl(var(--chart-1))',
+ '2': 'hsl(var(--chart-2))',
+ '3': 'hsl(var(--chart-3))',
+ '4': 'hsl(var(--chart-4))',
+ '5': 'hsl(var(--chart-5))'
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))'
+ }
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)'
+ }
+ }
+ },
+ plugins: [require("tailwindcss-animate")],
+};
+export default config;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..e7ff90f
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/frontend/types/index.ts b/frontend/types/index.ts
new file mode 100644
index 0000000..61b6dee
--- /dev/null
+++ b/frontend/types/index.ts
@@ -0,0 +1,8 @@
+export enum MenuId {
+ DASHBOARD = 'dashboard',
+ RESULTS = 'results',
+ EXPERIMENT = 'experiment',
+ REALTIME = 'realtime',
+ SEARCH = 'search',
+ SETTINGS = 'settings',
+}
\ No newline at end of file
diff --git a/generated-reports/experimentsreport_2024-11-17_15-10-45.yaml b/generated-reports/experimentsreport_2024-11-17_15-10-45.yaml
new file mode 100644
index 0000000..cd73f8a
--- /dev/null
+++ b/generated-reports/experimentsreport_2024-11-17_15-10-45.yaml
@@ -0,0 +1,598 @@
+report:
+ runs:
+ 6213b211:
+ interactions:
+ interaction_0:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: frontend_traces.duration
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: TraceResponseVariable
+ p_value: nan
+ test_statistic: nan
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/frontend_traces
+ interaction_1:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: system_CPU
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.1835768889085734e-09'
+ test_statistic: '8.42768994301385'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/system_CPU
+ interaction_2:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: recommendation_deployment_CPU
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.1240210033928467e-05'
+ test_statistic: '5.189436726580615'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/recommendation_deployment_CPU
+ interaction_3:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: frontend_http_latency
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.2805061129403184e-06'
+ test_statistic: '5.350568314134805'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/frontend_http_latency
+ interaction_4:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: cart_service_latency
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '0.2628979072383011'
+ test_statistic: '-1.1326424528806103'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/cart_service_latency
+ interaction_5:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: product_catalog_latency
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.6214677569971835e-06'
+ test_statistic: '5.153436579331668'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/product_catalog_latency
+ interaction_6:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: pod_status
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.0'
+ test_statistic: '0.0'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/pod_status
+ interaction_7:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: pod_restarts
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.2894930286085266e-05'
+ test_statistic: '5.07350131461379'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/pod_restarts
+ interaction_8:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: terminated_pods
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '0.02510043639313345'
+ test_statistic: '2.2511767167793586'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/terminated_pods
+ interaction_9:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: failed_spans
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: nan
+ test_statistic: nan
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/failed_spans
+ interaction_10:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: network_bytes
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '5.989547442105404e-08'
+ test_statistic: '7.077075743383718'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/network_bytes
+ interaction_11:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: network_drops
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: nan
+ test_statistic: nan
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/network_drops
+ interaction_12:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: network_errors
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: nan
+ test_statistic: nan
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/network_errors
+ interaction_13:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: tcp_retransmissions
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '3.649242681473021e-06'
+ test_statistic: '5.334203933234934'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/tcp_retransmissions
+ interaction_14:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: tcp_udp_errors
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: nan
+ test_statistic: nan
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/tcp_udp_errors
+ interaction_15:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: node_load
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '6.44858074992566e-06'
+ test_statistic: '5.255753672490723'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/node_load
+ interaction_16:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: memory_available
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '0.21332039574742578'
+ test_statistic: '1.247024696385633'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/memory_available
+ interaction_17:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: cpu_usage
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '2.5340110014897062e-08'
+ test_statistic: '7.333157881667362'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/cpu_usage
+ interaction_18:
+ treatment_name: empty_treatment
+ treatment_start: 2024-11-17 15:10:56.143224
+ treatment_end: 2024-11-17 15:15:56.143810
+ treatment_type: EmptyTreatment
+ response_name: tcp_connections
+ response_start: 2024-11-17 15:10:36.141508
+ response_end: 2024-11-17 15:16:06.144440
+ response_type: MetricResponseVariable
+ p_value: '1.1630126172857776e-09'
+ test_statistic: '6.284151974430294'
+ test_performed: welch t-test
+ store_key: /tmp/latest.yml/6213b211/tcp_connections
+ loadgen:
+ loadgen_start_time: 2024-11-17 15:10:46.141792
+ loadgen_end_time: 2024-11-17 15:16:07.459476
+ loadgen_total_requests: 39533
+ loadgen_total_failures: 103
+ task_details:
+ f620772fe02b4bad:
+ url: /
+ verb: GET
+ requests: 1614
+ failures: 92
+ fail_ratio: 0.057001239157372985
+ sum_response_time: 3968926.779601016
+ min_response_time: 14.8613329993168
+ max_response_time: 15199.399483999514
+ avg_response_time: 2459.062440892823
+ median_response_time: 99
+ e1842e9836b44386:
+ url: /api/products/2ZYFJ3GM2N
+ verb: GET
+ requests: 1822
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 134340.56113598286
+ min_response_time: 5.830048000461829
+ max_response_time: 11412.889264999649
+ avg_response_time: 73.73247043687314
+ median_response_time: 22
+ 951f750795404c1e:
+ url: /api/products/9SIQT8TOJO
+ verb: GET
+ requests: 1856
+ failures: 1
+ fail_ratio: 0.0005387931034482759
+ sum_response_time: 190099.3508510004
+ min_response_time: 5.359726999813574
+ max_response_time: 14487.208700000338
+ avg_response_time: 102.42421920851315
+ median_response_time: 23
+ d23b4e459c674516:
+ url: /api/recommendations?productIds=L9ECAV7KIM
+ verb: GET
+ requests: 340
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 39679.52797099224
+ min_response_time: 16.583272999923793
+ max_response_time: 4198.7926589999915
+ avg_response_time: 116.70449403233012
+ median_response_time: 61
+ abda553c264542cd:
+ url: /api/cart
+ verb: GET
+ requests: 3519
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 380767.450607043
+ min_response_time: 6.610173999433755
+ max_response_time: 14722.504982999453
+ avg_response_time: 108.20331077210656
+ median_response_time: 31
+ a653733f67d049cc:
+ url: /api/data/?contextKeys=travel
+ verb: GET
+ requests: 537
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 86054.36882400613
+ min_response_time: 11.315491999994265
+ max_response_time: 7943.602825999733
+ avg_response_time: 160.25022127375442
+ median_response_time: 44
+ 721fe4eefcee4670:
+ url: /api/data/?contextKeys=binoculars
+ verb: GET
+ requests: 497
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 55932.699639984094
+ min_response_time: 10.142383000129485
+ max_response_time: 5147.699707999891
+ avg_response_time: 112.54064313880099
+ median_response_time: 37
+ 04426bbd8c2d4fc0:
+ url: /api/products/L9ECAV7KIM
+ verb: GET
+ requests: 1829
+ failures: 1
+ fail_ratio: 0.0005467468562055768
+ sum_response_time: 133881.4136250146
+ min_response_time: 5.651485999806027
+ max_response_time: 6306.889313999818
+ avg_response_time: 73.19924200383521
+ median_response_time: 23
+ 73b442b8c7204ef9:
+ url: /api/recommendations?productIds=HQTGWGPNH4
+ verb: GET
+ requests: 335
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 52540.574407998974
+ min_response_time: 15.624447999471158
+ max_response_time: 7513.003635999667
+ avg_response_time: 156.8375355462656
+ median_response_time: 60
+ 1d73356d5acd4a56:
+ url: /api/products/HQTGWGPNH4
+ verb: GET
+ requests: 1805
+ failures: 1
+ fail_ratio: 0.000554016620498615
+ sum_response_time: 185001.11546202155
+ min_response_time: 5.528152999431768
+ max_response_time: 15039.695731999927
+ avg_response_time: 102.49369277674325
+ median_response_time: 24
+ 19e7fc7bcc3a4867:
+ url: /api/products/1YMWWN1N4O
+ verb: GET
+ requests: 1778
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 128815.3319089988
+ min_response_time: 5.506286000127147
+ max_response_time: 7103.7086949991135
+ avg_response_time: 72.44956800281147
+ median_response_time: 22
+ 4a3b026bf8fb4a71:
+ url: /api/recommendations?productIds=0PUK6V6EV0
+ verb: GET
+ requests: 353
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 56248.76167899674
+ min_response_time: 17.32514400009677
+ max_response_time: 6698.671359999935
+ avg_response_time: 159.34493393483496
+ median_response_time: 63
+ 0d7cecf3052944eb:
+ url: /api/cart
+ verb: POST
+ requests: 6822
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 522413.3973910225
+ min_response_time: 9.370721999403031
+ max_response_time: 7128.00765800057
+ avg_response_time: 76.57774807842604
+ median_response_time: 40
+ c7ece7a4bb184a8e:
+ url: /api/products/0PUK6V6EV0
+ verb: GET
+ requests: 1869
+ failures: 1
+ fail_ratio: 0.0005350454788657035
+ sum_response_time: 162667.26346899476
+ min_response_time: 5.959510999673512
+ max_response_time: 15005.757705000178
+ avg_response_time: 87.03438387854187
+ median_response_time: 22
+ c5e9131131d54611:
+ url: /api/recommendations?productIds=66VCHSJNUP
+ verb: GET
+ requests: 362
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 77451.74545998174
+ min_response_time: 16.39168800011248
+ max_response_time: 14830.673643999944
+ avg_response_time: 213.95509795575066
+ median_response_time: 65
+ 0f6ab89d6c7a4b2e:
+ url: /api/products/LS4PSXUNUM
+ verb: GET
+ requests: 1809
+ failures: 1
+ fail_ratio: 0.000552791597567717
+ sum_response_time: 154434.46581100422
+ min_response_time: 5.494768000062322
+ max_response_time: 8344.624629999998
+ avg_response_time: 85.37007507518199
+ median_response_time: 23
+ 05c86cc2eb024107:
+ url: /api/checkout
+ verb: POST
+ requests: 2243
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 371218.2931120036
+ min_response_time: 37.27922799953376
+ max_response_time: 7524.132222999469
+ avg_response_time: 165.50079942577065
+ median_response_time: 98
+ d57f9ad1bf4b4475:
+ url: /api/products/OLJCESPC7Z
+ verb: GET
+ requests: 1865
+ failures: 2
+ fail_ratio: 0.0010723860589812334
+ sum_response_time: 136678.21693601742
+ min_response_time: 7.085987999744248
+ max_response_time: 7070.02362700041
+ avg_response_time: 73.28590720429888
+ median_response_time: 27
+ 742b7837452943f0:
+ url: /api/data/?contextKeys=assembly
+ verb: GET
+ requests: 495
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 58604.67978499946
+ min_response_time: 9.509473999969487
+ max_response_time: 7686.769871999786
+ avg_response_time: 118.39329249494841
+ median_response_time: 41
+ 83c4ba5716a14b3a:
+ url: /api/recommendations?productIds=6E92ZMYYFZ
+ verb: GET
+ requests: 338
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 51501.95566801722
+ min_response_time: 16.50879499993607
+ max_response_time: 4683.115364000514
+ avg_response_time: 152.3726499053764
+ median_response_time: 60
+ 3367a81d81b44858:
+ url: /api/recommendations?productIds=LS4PSXUNUM
+ verb: GET
+ requests: 330
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 54866.44667299788
+ min_response_time: 16.697590999683598
+ max_response_time: 8147.541925000041
+ avg_response_time: 166.2619596151451
+ median_response_time: 66
+ 5300c9ca3c29450e:
+ url: /api/products/66VCHSJNUP
+ verb: GET
+ requests: 1903
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 216720.09615198182
+ min_response_time: 5.4609540002275025
+ max_response_time: 11380.292308000207
+ avg_response_time: 113.88339261796207
+ median_response_time: 24
+ 190f5814a783448a:
+ url: /api/products/6E92ZMYYFZ
+ verb: GET
+ requests: 1842
+ failures: 2
+ fail_ratio: 0.0010857763300760044
+ sum_response_time: 167174.6862540131
+ min_response_time: 5.707530000108818
+ max_response_time: 8291.291339000054
+ avg_response_time: 90.7571586612449
+ median_response_time: 23
+ d9e9aead8a834763:
+ url: /api/data/?contextKeys=accessories
+ verb: GET
+ requests: 503
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 68265.33427501179
+ min_response_time: 10.109556000315933
+ max_response_time: 9876.570320999235
+ avg_response_time: 135.71637032805523
+ median_response_time: 40
+ 9480be7fb57e4ea8:
+ url: /api/recommendations?productIds=9SIQT8TOJO
+ verb: GET
+ requests: 337
+ failures: 2
+ fail_ratio: 0.005934718100890208
+ sum_response_time: 53670.99093800334
+ min_response_time: 17.387206999956106
+ max_response_time: 15053.400941999826
+ avg_response_time: 159.26110070624136
+ median_response_time: 68
+ 6bb9c25d9260436d:
+ url: /api/recommendations?productIds=OLJCESPC7Z
+ verb: GET
+ requests: 334
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 44951.35387199571
+ min_response_time: 17.61290000013105
+ max_response_time: 3632.841276000363
+ avg_response_time: 134.58489183232248
+ median_response_time: 68
+ 382d1a39534a4f73:
+ url: /api/data/
+ verb: GET
+ requests: 511
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 67656.6994919995
+ min_response_time: 9.892730999126798
+ max_response_time: 7502.767044999928
+ avg_response_time: 132.4005860900186
+ median_response_time: 41
+ c2761755cad24dcb:
+ url: /api/data/?contextKeys=telescopes
+ verb: GET
+ requests: 481
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 61279.05572798227
+ min_response_time: 10.036269000011089
+ max_response_time: 14572.33039499988
+ avg_response_time: 127.39928425775939
+ median_response_time: 42
+ a1a948e946654089:
+ url: /api/recommendations?productIds=2ZYFJ3GM2N
+ verb: GET
+ requests: 356
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 44057.02746900715
+ min_response_time: 17.44104800036439
+ max_response_time: 2230.6932669998787
+ avg_response_time: 123.75569513766054
+ median_response_time: 69
+ e176a6e632c04ecb:
+ url: /api/data/?contextKeys=books
+ verb: GET
+ requests: 481
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 60189.10683499962
+ min_response_time: 9.575883999787038
+ max_response_time: 9723.475677999886
+ avg_response_time: 125.13327824324246
+ median_response_time: 41
+ edee1648560e4257:
+ url: /api/recommendations?productIds=1YMWWN1N4O
+ verb: GET
+ requests: 367
+ failures: 0
+ fail_ratio: 0.0
+ sum_response_time: 61698.10033401245
+ min_response_time: 16.27876099973946
+ max_response_time: 8285.172294000404
+ avg_response_time: 168.11471480657343
+ median_response_time: 61
diff --git a/kepler_dashboard.json b/k8s/dashboards/kepler_dashboard.json
similarity index 100%
rename from kepler_dashboard.json
rename to k8s/dashboards/kepler_dashboard.json
diff --git a/jaeger_pvc.yaml b/k8s/manifests/jaeger_pvc.yaml
similarity index 100%
rename from jaeger_pvc.yaml
rename to k8s/manifests/jaeger_pvc.yaml
diff --git a/k8s/manifests/simple_fault_detection_rules.yml b/k8s/manifests/simple_fault_detection_rules.yml
new file mode 100644
index 0000000..37c97f5
--- /dev/null
+++ b/k8s/manifests/simple_fault_detection_rules.yml
@@ -0,0 +1,583 @@
+groups:
+ - name: KubestateExporter
+
+ rules:
+ - alert: KubernetesNodeNotReady
+ expr: 'kube_node_status_condition{condition="Ready",status="true"} == 0'
+ for: 10m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Node ready (node {{ $labels.node }})
+ description: "Node {{ $labels.node }} has been unready for a long time\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeMemoryPressure
+ expr: 'kube_node_status_condition{condition="MemoryPressure",status="true"} == 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes memory pressure (node {{ $labels.node }})
+ description: "Node {{ $labels.node }} has MemoryPressure condition\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeDiskPressure
+ expr: 'kube_node_status_condition{condition="DiskPressure",status="true"} == 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes disk pressure (node {{ $labels.node }})
+ description: "Node {{ $labels.node }} has DiskPressure condition\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeNetworkUnavailable
+ expr: 'kube_node_status_condition{condition="NetworkUnavailable",status="true"} == 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Node network unavailable (instance {{ $labels.instance }})
+ description: "Node {{ $labels.node }} has NetworkUnavailable condition\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeOutOfPodCapacity
+ expr: 'sum by (node) ((kube_pod_status_phase{phase="Running"} == 1) + on(uid) group_left(node) (0 * kube_pod_info{pod_template_hash=""})) / sum by (node) (kube_node_status_allocatable{resource="pods"}) * 100 > 90'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Node out of pod capacity (instance {{ $labels.instance }})
+ description: "Node {{ $labels.node }} is out of pod capacity\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesContainerOomKiller
+ expr: '(kube_pod_container_status_restarts_total - kube_pod_container_status_restarts_total offset 10m >= 1) and ignoring (reason) min_over_time(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[10m]) == 1'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes container oom killer ({{ $labels.namespace }}/{{ $labels.pod }}:{{ $labels.container }})
+ description: "Container {{ $labels.container }} in pod {{ $labels.namespace }}/{{ $labels.pod }} has been OOMKilled {{ $value }} times in the last 10 minutes.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesJobFailed
+ expr: "kube_job_status_failed > 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Job failed ({{ $labels.namespace }}/{{ $labels.job_name }})
+ description: "Job {{ $labels.namespace }}/{{ $labels.job_name }} failed to complete\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesJobNotStarting
+ expr: "kube_job_status_active == 0 and kube_job_status_failed == 0 and kube_job_status_succeeded == 0 and (time() - kube_job_status_start_time) > 600"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Job not starting ({{ $labels.namespace }}/{{ $labels.job_name }})
+ description: "Job {{ $labels.namespace }}/{{ $labels.job_name }} did not start for 10 minutes\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesCronjobSuspended
+ expr: "kube_cronjob_spec_suspend != 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes CronJob suspended ({{ $labels.namespace }}/{{ $labels.cronjob }})
+ description: "CronJob {{ $labels.namespace }}/{{ $labels.cronjob }} is suspended\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPersistentvolumeclaimPending
+ expr: 'kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes PersistentVolumeClaim pending ({{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }})
+ description: "PersistentVolumeClaim {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is pending\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesVolumeOutOfDiskSpace
+ expr: "kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes * 100 < 10"
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Volume out of disk space (instance {{ $labels.instance }})
+ description: "Volume is almost full (< 10% left)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesVolumeFullInFourDays
+ expr: "predict_linear(kubelet_volume_stats_available_bytes[6h:5m], 4 * 24 * 3600) < 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Volume full in four days (instance {{ $labels.instance }})
+ description: "Volume under {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is expected to fill up within four days. Currently {{ $value | humanize }}% is available.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPersistentvolumeError
+ expr: 'kube_persistentvolume_status_phase{phase=~"Failed|Pending", job="kube-state-metrics"} > 0'
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes PersistentVolumeClaim pending ({{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }})
+ description: "Persistent volume {{ $labels.persistentvolume }} is in bad state\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetDown
+ expr: "kube_statefulset_replicas != kube_statefulset_status_replicas_ready > 0"
+ for: 1m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes StatefulSet down ({{ $labels.namespace }}/{{ $labels.statefulset }})
+ description: "StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} went down\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaScaleInability
+ expr: '(kube_horizontalpodautoscaler_spec_max_replicas - kube_horizontalpodautoscaler_status_desired_replicas) * on (horizontalpodautoscaler,namespace) (kube_horizontalpodautoscaler_status_condition{condition="ScalingLimited", status="true"} == 1) == 0'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes HPA scale inability (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} is unable to scale\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaMetricsUnavailability
+ expr: 'kube_horizontalpodautoscaler_status_condition{status="false", condition="ScalingActive"} == 1'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes HPA metrics unavailability (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} is unable to collect metrics\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaScaleMaximum
+ expr: "(kube_horizontalpodautoscaler_status_desired_replicas >= kube_horizontalpodautoscaler_spec_max_replicas) and (kube_horizontalpodautoscaler_spec_max_replicas > 1) and (kube_horizontalpodautoscaler_spec_min_replicas != kube_horizontalpodautoscaler_spec_max_replicas)"
+ for: 2m
+ labels:
+ severity: info
+ annotations:
+ summary: Kubernetes HPA scale maximum (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} has hit maximum number of desired pods\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaUnderutilized
+ expr: "max(quantile_over_time(0.5, kube_horizontalpodautoscaler_status_desired_replicas[1d]) == kube_horizontalpodautoscaler_spec_min_replicas) by (horizontalpodautoscaler) > 3"
+ for: 0m
+ labels:
+ severity: info
+ annotations:
+ summary: Kubernetes HPA underutilized (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} is constantly at minimum replicas for 50% of the time. Potential cost saving here.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPodNotHealthy
+ expr: 'sum by (namespace, pod) (kube_pod_status_phase{phase=~"Pending|Unknown|Failed"}) > 0'
+ for: 15m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Pod not healthy ({{ $labels.namespace }}/{{ $labels.pod }})
+ description: "Pod {{ $labels.namespace }}/{{ $labels.pod }} has been in a non-running state for longer than 15 minutes.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPodCrashLooping
+ expr: "increase(kube_pod_container_status_restarts_total[1m]) > 3"
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes pod crash looping ({{ $labels.namespace }}/{{ $labels.pod }})
+ description: "Pod {{ $labels.namespace }}/{{ $labels.pod }} is crash looping\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesReplicasetReplicasMismatch
+ expr: "kube_replicaset_spec_replicas != kube_replicaset_status_ready_replicas"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes ReplicasSet mismatch ({{ $labels.namespace }}/{{ $labels.replicaset }})
+ description: "ReplicaSet {{ $labels.namespace }}/{{ $labels.replicaset }} replicas mismatch\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDeploymentReplicasMismatch
+ expr: "kube_deployment_spec_replicas != kube_deployment_status_replicas_available"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Deployment replicas mismatch ({{ $labels.namespace }}/{{ $labels.deployment }})
+ description: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} replicas mismatch\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetReplicasMismatch
+ expr: "kube_statefulset_status_replicas_ready != kube_statefulset_status_replicas"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes StatefulSet replicas mismatch (instance {{ $labels.instance }})
+ description: "StatefulSet does not match the expected number of replicas.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDeploymentGenerationMismatch
+ expr: "kube_deployment_status_observed_generation != kube_deployment_metadata_generation"
+ for: 10m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Deployment generation mismatch ({{ $labels.namespace }}/{{ $labels.deployment }})
+ description: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} has failed but has not been rolled back.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetGenerationMismatch
+ expr: "kube_statefulset_status_observed_generation != kube_statefulset_metadata_generation"
+ for: 10m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes StatefulSet generation mismatch ({{ $labels.namespace }}/{{ $labels.statefulset }})
+ description: "StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} has failed but has not been rolled back.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetUpdateNotRolledOut
+ expr: "max without (revision) (kube_statefulset_status_current_revision unless kube_statefulset_status_update_revision) * (kube_statefulset_replicas != kube_statefulset_status_replicas_updated)"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes StatefulSet update not rolled out ({{ $labels.namespace }}/{{ $labels.statefulset }})
+ description: "StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} update has not been rolled out.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDaemonsetRolloutStuck
+ expr: "kube_daemonset_status_number_ready / kube_daemonset_status_desired_number_scheduled * 100 < 100 or kube_daemonset_status_desired_number_scheduled - kube_daemonset_status_current_number_scheduled > 0"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes DaemonSet rollout stuck ({{ $labels.namespace }}/{{ $labels.daemonset }})
+ description: "Some Pods of DaemonSet {{ $labels.namespace }}/{{ $labels.daemonset }} are not scheduled or not ready\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDaemonsetMisscheduled
+ expr: "kube_daemonset_status_number_misscheduled > 0"
+ for: 1m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes DaemonSet misscheduled ({{ $labels.namespace }}/{{ $labels.daemonset }})
+ description: "Some Pods of DaemonSet {{ $labels.namespace }}/{{ $labels.daemonset }} are running where they are not supposed to run\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesCronjobTooLong
+ expr: "time() - kube_cronjob_next_schedule_time > 3600"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes CronJob too long ({{ $labels.namespace }}/{{ $labels.cronjob }})
+ description: "CronJob {{ $labels.namespace }}/{{ $labels.cronjob }} is taking more than 1h to complete.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesJobSlowCompletion
+ expr: "kube_job_spec_completions - kube_job_status_succeeded - kube_job_status_failed > 0"
+ for: 12h
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes job slow completion ({{ $labels.namespace }}/{{ $labels.job_name }})
+ description: "Kubernetes Job {{ $labels.namespace }}/{{ $labels.job_name }} did not complete in time.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesApiServerErrors
+ expr: 'sum(rate(apiserver_request_total{job="apiserver",code=~"(?:5..)"}[1m])) by (instance, job) / sum(rate(apiserver_request_total{job="apiserver"}[1m])) by (instance, job) * 100 > 3'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes API server errors (instance {{ $labels.instance }})
+ description: "Kubernetes API server is experiencing high error rate\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesApiClientErrors
+ expr: '(sum(rate(rest_client_requests_total{code=~"(4|5).."}[1m])) by (instance, job) / sum(rate(rest_client_requests_total[1m])) by (instance, job)) * 100 > 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes API client errors (instance {{ $labels.instance }})
+ description: "Kubernetes API client is experiencing high error rate\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesClientCertificateExpiresNextWeek
+ expr: 'apiserver_client_certificate_expiration_seconds_count{job="apiserver"} > 0 and histogram_quantile(0.01, sum by (job, le) (rate(apiserver_client_certificate_expiration_seconds_bucket{job="apiserver"}[5m]))) < 7*24*60*60'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes client certificate expires next week (instance {{ $labels.instance }})
+ description: "A client certificate used to authenticate to the apiserver is expiring next week.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesClientCertificateExpiresSoon
+ expr: 'apiserver_client_certificate_expiration_seconds_count{job="apiserver"} > 0 and histogram_quantile(0.01, sum by (job, le) (rate(apiserver_client_certificate_expiration_seconds_bucket{job="apiserver"}[5m]))) < 24*60*60'
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes client certificate expires soon (instance {{ $labels.instance }})
+ description: "A client certificate used to authenticate to the apiserver is expiring in less than 24.0 hours.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesApiServerLatency
+ expr: 'histogram_quantile(0.99, sum(rate(apiserver_request_duration_seconds_bucket{verb!~"(?:CONNECT|WATCHLIST|WATCH|PROXY)"} [10m])) WITHOUT (subresource)) > 1'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes API server latency (instance {{ $labels.instance }})
+ description: "Kubernetes API server has a 99th percentile latency of {{ $value }} seconds for {{ $labels.verb }} {{ $labels.resource }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - name: CoreDNS
+ rules:
+ - alert: CorednsPanicCount
+ expr: increase(coredns_panics_total[1m]) > 0
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: CoreDNS Panic Count (instance {{ $labels.instance }})
+ description: "Number of CoreDNS panics encountered\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - name: PrometheusSelfMonitoring
+ rules:
+ - alert: PrometheusJobMissing
+ expr: 'absent(up{job="prometheus"})'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus job missing (instance {{ $labels.instance }})
+ description: "A Prometheus job has disappeared\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetMissing
+ expr: "up == 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus target missing (instance {{ $labels.instance }})
+ description: "A Prometheus target has disappeared. An exporter might be crashed.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAllTargetsMissing
+ expr: "sum by (job) (up) == 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus all targets missing (instance {{ $labels.instance }})
+ description: "A Prometheus job does not have living target anymore.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetMissingWithWarmupTime
+ expr: "sum by (instance, job) ((up == 0) * on (instance) group_right(job) (node_time_seconds - node_boot_time_seconds > 600))"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus target missing with warmup time (instance {{ $labels.instance }})
+ description: "Allow a job time to start up (10 minutes) before alerting that it's down.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusConfigurationReloadFailure
+ expr: "prometheus_config_last_reload_successful != 1"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus configuration reload failure (instance {{ $labels.instance }})
+ description: "Prometheus configuration reload error\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTooManyRestarts
+ expr: 'changes(process_start_time_seconds{job=~"prometheus|pushgateway|alertmanager"}[15m]) > 2'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus too many restarts (instance {{ $labels.instance }})
+ description: "Prometheus has restarted more than twice in the last 15 minutes. It might be crashlooping.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerJobMissing
+ expr: 'absent(up{job="alertmanager"})'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus AlertManager job missing (instance {{ $labels.instance }})
+ description: "A Prometheus AlertManager job has disappeared\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerConfigurationReloadFailure
+ expr: "alertmanager_config_last_reload_successful != 1"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus AlertManager configuration reload failure (instance {{ $labels.instance }})
+ description: "AlertManager configuration reload error\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerConfigNotSynced
+ expr: 'count(count_values("config_hash", alertmanager_config_hash)) > 1'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus AlertManager config not synced (instance {{ $labels.instance }})
+ description: "Configurations of AlertManager cluster instances are out of sync\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerE2eDeadManSwitch
+ expr: "vector(1)"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus AlertManager E2E dead man switch (instance {{ $labels.instance }})
+ description: "Prometheus DeadManSwitch is an always-firing alert. It's used as an end-to-end test of Prometheus through the Alertmanager.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusNotConnectedToAlertmanager
+ expr: "prometheus_notifications_alertmanagers_discovered < 1"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus not connected to alertmanager (instance {{ $labels.instance }})
+ description: "Prometheus cannot connect the alertmanager\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusRuleEvaluationFailures
+ expr: "increase(prometheus_rule_evaluation_failures_total[3m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus rule evaluation failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} rule evaluation failures, leading to potentially ignored alerts.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTemplateTextExpansionFailures
+ expr: "increase(prometheus_template_text_expansion_failures_total[3m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus template text expansion failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} template text expansion failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusRuleEvaluationSlow
+ expr: "prometheus_rule_group_last_duration_seconds > prometheus_rule_group_interval_seconds"
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus rule evaluation slow (instance {{ $labels.instance }})
+ description: "Prometheus rule evaluation took more time than the scheduled interval. It indicates a slower storage backend access or too complex query.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusNotificationsBacklog
+ expr: "min_over_time(prometheus_notifications_queue_length[10m]) > 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus notifications backlog (instance {{ $labels.instance }})
+ description: "The Prometheus notification queue has not been empty for 10 minutes\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerNotificationFailing
+ expr: "rate(alertmanager_notifications_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus AlertManager notification failing (instance {{ $labels.instance }})
+ description: "Alertmanager is failing sending notifications\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetEmpty
+ expr: "prometheus_sd_discovered_targets == 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus target empty (instance {{ $labels.instance }})
+ description: "Prometheus has no target in service discovery\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetScrapingSlow
+ expr: 'prometheus_target_interval_length_seconds{quantile="0.9"} / on (interval, instance, job) prometheus_target_interval_length_seconds{quantile="0.5"} > 1.05'
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus target scraping slow (instance {{ $labels.instance }})
+ description: "Prometheus is scraping exporters slowly since it exceeded the requested interval time. Your Prometheus server is under-provisioned.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusLargeScrape
+ expr: "increase(prometheus_target_scrapes_exceeded_sample_limit_total[10m]) > 10"
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus large scrape (instance {{ $labels.instance }})
+ description: "Prometheus has many scrapes that exceed the sample limit\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetScrapeDuplicate
+ expr: "increase(prometheus_target_scrapes_sample_duplicate_timestamp_total[5m]) > 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus target scrape duplicate (instance {{ $labels.instance }})
+ description: "Prometheus has many samples rejected due to duplicate timestamps but different values\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbCheckpointCreationFailures
+ expr: "increase(prometheus_tsdb_checkpoint_creations_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB checkpoint creation failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} checkpoint creation failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbCheckpointDeletionFailures
+ expr: "increase(prometheus_tsdb_checkpoint_deletions_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB checkpoint deletion failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} checkpoint deletion failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbCompactionsFailed
+ expr: "increase(prometheus_tsdb_compactions_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB compactions failed (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB compactions failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbHeadTruncationsFailed
+ expr: "increase(prometheus_tsdb_head_truncations_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB head truncations failed (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB head truncation failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbReloadFailures
+ expr: "increase(prometheus_tsdb_reloads_failures_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB reload failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB reload failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbWalCorruptions
+ expr: "increase(prometheus_tsdb_wal_corruptions_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB WAL corruptions (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB WAL corruptions\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbWalTruncationsFailed
+ expr: "increase(prometheus_tsdb_wal_truncations_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB WAL truncations failed (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB WAL truncation failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTimeseriesCardinality
+ expr: 'label_replace(count by(__name__) ({__name__=~".+"}), "name", "$1", "__name__", "(.+)") > 10000'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus timeseries cardinality (instance {{ $labels.instance }})
+ description: "The \"{{ $labels.name }}\" timeseries cardinality is getting very high: {{ $value }}\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
diff --git a/values_kepler.yaml b/k8s/manifests/values_kepler.yaml
similarity index 100%
rename from values_kepler.yaml
rename to k8s/manifests/values_kepler.yaml
diff --git a/values_kube_prometheus.yaml b/k8s/manifests/values_kube_prometheus.yaml
similarity index 100%
rename from values_kube_prometheus.yaml
rename to k8s/manifests/values_kube_prometheus.yaml
diff --git a/k8s/manifests/values_opentelemetry_demo.yaml b/k8s/manifests/values_opentelemetry_demo.yaml
new file mode 100644
index 0000000..fd9867d
--- /dev/null
+++ b/k8s/manifests/values_opentelemetry_demo.yaml
@@ -0,0 +1,728 @@
+jaeger:
+ allInOne:
+ resources:
+ requests:
+ memory: 5Gi
+ limits:
+ memory: 5Gi
+ # args:
+ # - --query.base-path=/jaeger/ui
+ # - --prometheus.server-url=http://{{ include "otel-demo.name" . }}-prometheus-server:9090
+ # - --prometheus.query.normalize-calls=true
+ # - --prometheus.query.normalize-duration=true
+ # storage:
+ # badger:
+ # ephemeral: false
+ # persistence:
+ # mountPath: /mnt/data
+ # useExistingPvcName: "jaeger-pvc"
+ # type: badger
+
+components:
+ flagd:
+ resources:
+ limits:
+ memory: 150Mi
+ productCatalogService:
+ resources:
+ limits:
+ memory: 150Mi
+ loadgenerator:
+ enabled: false
+ checkoutService:
+ resources:
+ limits:
+ memory: 150Mi
+
+opentelemetry-collector:
+ config:
+ processors:
+ probabilistic_sampler:
+ sampling_percentage: 1
+ hash_seed: 22
+ service:
+ pipelines:
+ traces:
+ processors:
+ - memory_limiter
+ - resource
+ - transform
+ - batch
+ - probabilistic_sampler
+prometheus:
+ server:
+ extraArgs:
+ web.enable-lifecycle: ""
+ persistentVolume:
+ enabled: true
+ storageClass: "openebs-hostpath"
+ accessModes:
+ - ReadWriteOnce
+ size: 8Gi
+ resources:
+ limits:
+ memory: 1500Mi
+ alertmanager:
+ enabled: false
+ prometheus-node-exporter:
+ enabled: false
+ podAnnotations:
+ prometheus.io/scrape: "true"
+ prometheus.io/port: "9100"
+ opentelemetry_community_demo: "true"
+ kube-state-metrics:
+ enabled: true
+ podAnnotations:
+ prometheus.io/scrape: "true"
+ prometheus.io/port: "8080"
+ opentelemetry_community_demo: "true"
+ serverFiles:
+ prometheus.yml:
+ #rule_files:
+ # - "simple_fault_detection_rules.yml"
+ scrape_configs:
+ - job_name: "kubernetes-pods"
+ kubernetes_sd_configs:
+ - role: pod
+ relabel_configs:
+ - source_labels:
+ [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
+ action: keep
+ regex: true
+ - job_name: "otel-collector"
+ honor_labels: true
+ kubernetes_sd_configs:
+ - role: pod
+ namespaces:
+ own_namespace: true
+ relabel_configs:
+ - source_labels:
+ [__meta_kubernetes_pod_annotation_opentelemetry_community_demo]
+ action: keep
+ regex: true
+ - job_name: "kubernetes-service-endpoints"
+ scrape_interval: 5s
+ scrape_timeout: 2s
+ kubernetes_sd_configs:
+ - role: service
+ relabel_configs:
+ - source_labels:
+ [__meta_kubernetes_service_annotation_prometheus_io_scrape]
+ action: keep
+ regex: true
+ - source_labels:
+ [__meta_kubernetes_service_annotation_prometheus_io_scheme]
+ action: replace
+ target_label: __scheme__
+ regex: (https?)
+ - source_labels:
+ [__meta_kubernetes_service_annotation_prometheus_io_path]
+ action: replace
+ target_label: __metrics_path__
+ regex: (.+)
+ - source_labels:
+ [
+ __address__,
+ __meta_kubernetes_service_annotation_prometheus_io_port,
+ ]
+ action: replace
+ target_label: __address__
+ regex: (.+)(?::\d+);(\d+)
+ replacement: $1:$2
+ - action: labelmap
+ regex: __meta_kubernetes_service_label_(.+)
+ - source_labels: [__meta_kubernetes_namespace]
+ action: replace
+ target_label: kubernetes_namespace
+ - source_labels: [__meta_kubernetes_service_name]
+ action: replace
+ target_label: kubernetes_name
+ - source_labels:
+ [__meta_kubernetes_service_annotation_prometheus_io_scheme]
+ action: replace
+ target_label: __scheme__
+ regex: (.+)
+ alerting_rules.yml:
+ groups:
+ - name: KubestateExporter
+
+ rules:
+ - alert: KubernetesNodeNotReady
+ expr: 'kube_node_status_condition{condition="Ready",status="true"} == 0'
+ for: 10m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Node ready (node {{ $labels.node }})
+ description: "Node {{ $labels.node }} has been unready for a long time\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeMemoryPressure
+ expr: 'kube_node_status_condition{condition="MemoryPressure",status="true"} == 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes memory pressure (node {{ $labels.node }})
+ description: "Node {{ $labels.node }} has MemoryPressure condition\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeDiskPressure
+ expr: 'kube_node_status_condition{condition="DiskPressure",status="true"} == 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes disk pressure (node {{ $labels.node }})
+ description: "Node {{ $labels.node }} has DiskPressure condition\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeNetworkUnavailable
+ expr: 'kube_node_status_condition{condition="NetworkUnavailable",status="true"} == 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Node network unavailable (instance {{ $labels.instance }})
+ description: "Node {{ $labels.node }} has NetworkUnavailable condition\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesNodeOutOfPodCapacity
+ expr: 'sum by (node) ((kube_pod_status_phase{phase="Running"} == 1) + on(uid) group_left(node) (0 * kube_pod_info{pod_template_hash=""})) / sum by (node) (kube_node_status_allocatable{resource="pods"}) * 100 > 90'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Node out of pod capacity (instance {{ $labels.instance }})
+ description: "Node {{ $labels.node }} is out of pod capacity\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesContainerOomKiller
+ expr: '(kube_pod_container_status_restarts_total - kube_pod_container_status_restarts_total offset 10m >= 1) and ignoring (reason) min_over_time(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[10m]) == 1'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes container oom killer ({{ $labels.namespace }}/{{ $labels.pod }}:{{ $labels.container }})
+ description: "Container {{ $labels.container }} in pod {{ $labels.namespace }}/{{ $labels.pod }} has been OOMKilled {{ $value }} times in the last 10 minutes.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesJobFailed
+ expr: "kube_job_status_failed > 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Job failed ({{ $labels.namespace }}/{{ $labels.job_name }})
+ description: "Job {{ $labels.namespace }}/{{ $labels.job_name }} failed to complete\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesJobNotStarting
+ expr: "kube_job_status_active == 0 and kube_job_status_failed == 0 and kube_job_status_succeeded == 0 and (time() - kube_job_status_start_time) > 600"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Job not starting ({{ $labels.namespace }}/{{ $labels.job_name }})
+ description: "Job {{ $labels.namespace }}/{{ $labels.job_name }} did not start for 10 minutes\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesCronjobSuspended
+ expr: "kube_cronjob_spec_suspend != 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes CronJob suspended ({{ $labels.namespace }}/{{ $labels.cronjob }})
+ description: "CronJob {{ $labels.namespace }}/{{ $labels.cronjob }} is suspended\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPersistentvolumeclaimPending
+ expr: 'kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes PersistentVolumeClaim pending ({{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }})
+ description: "PersistentVolumeClaim {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is pending\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesVolumeOutOfDiskSpace
+ expr: "kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes * 100 < 10"
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Volume out of disk space (instance {{ $labels.instance }})
+ description: "Volume is almost full (< 10% left)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesVolumeFullInFourDays
+ expr: "predict_linear(kubelet_volume_stats_available_bytes[6h:5m], 4 * 24 * 3600) < 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Volume full in four days (instance {{ $labels.instance }})
+ description: "Volume under {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is expected to fill up within four days. Currently {{ $value | humanize }}% is available.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPersistentvolumeError
+ expr: 'kube_persistentvolume_status_phase{phase=~"Failed|Pending", job="kube-state-metrics"} > 0'
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes PersistentVolumeClaim pending ({{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }})
+ description: "Persistent volume {{ $labels.persistentvolume }} is in bad state\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetDown
+ expr: "kube_statefulset_replicas != kube_statefulset_status_replicas_ready > 0"
+ for: 1m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes StatefulSet down ({{ $labels.namespace }}/{{ $labels.statefulset }})
+ description: "StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} went down\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaScaleInability
+ expr: '(kube_horizontalpodautoscaler_spec_max_replicas - kube_horizontalpodautoscaler_status_desired_replicas) * on (horizontalpodautoscaler,namespace) (kube_horizontalpodautoscaler_status_condition{condition="ScalingLimited", status="true"} == 1) == 0'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes HPA scale inability (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} is unable to scale\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaMetricsUnavailability
+ expr: 'kube_horizontalpodautoscaler_status_condition{status="false", condition="ScalingActive"} == 1'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes HPA metrics unavailability (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} is unable to collect metrics\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaScaleMaximum
+ expr: "(kube_horizontalpodautoscaler_status_desired_replicas >= kube_horizontalpodautoscaler_spec_max_replicas) and (kube_horizontalpodautoscaler_spec_max_replicas > 1) and (kube_horizontalpodautoscaler_spec_min_replicas != kube_horizontalpodautoscaler_spec_max_replicas)"
+ for: 2m
+ labels:
+ severity: info
+ annotations:
+ summary: Kubernetes HPA scale maximum (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} has hit maximum number of desired pods\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesHpaUnderutilized
+ expr: "max(quantile_over_time(0.5, kube_horizontalpodautoscaler_status_desired_replicas[1d]) == kube_horizontalpodautoscaler_spec_min_replicas) by (horizontalpodautoscaler) > 3"
+ for: 0m
+ labels:
+ severity: info
+ annotations:
+ summary: Kubernetes HPA underutilized (instance {{ $labels.instance }})
+ description: "HPA {{ $labels.namespace }}/{{ $labels.horizontalpodautoscaler }} is constantly at minimum replicas for 50% of the time. Potential cost saving here.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPodNotHealthy
+ expr: 'sum by (namespace, pod) (kube_pod_status_phase{phase=~"Pending|Unknown|Failed"}) > 0'
+ for: 15m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Pod not healthy ({{ $labels.namespace }}/{{ $labels.pod }})
+ description: "Pod {{ $labels.namespace }}/{{ $labels.pod }} has been in a non-running state for longer than 15 minutes.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesPodCrashLooping
+ expr: "increase(kube_pod_container_status_restarts_total[1m]) > 3"
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes pod crash looping ({{ $labels.namespace }}/{{ $labels.pod }})
+ description: "Pod {{ $labels.namespace }}/{{ $labels.pod }} is crash looping\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesReplicasetReplicasMismatch
+ expr: "kube_replicaset_spec_replicas != kube_replicaset_status_ready_replicas"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes ReplicasSet mismatch ({{ $labels.namespace }}/{{ $labels.replicaset }})
+ description: "ReplicaSet {{ $labels.namespace }}/{{ $labels.replicaset }} replicas mismatch\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDeploymentReplicasMismatch
+ expr: "kube_deployment_spec_replicas != kube_deployment_status_replicas_available"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes Deployment replicas mismatch ({{ $labels.namespace }}/{{ $labels.deployment }})
+ description: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} replicas mismatch\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetReplicasMismatch
+ expr: "kube_statefulset_status_replicas_ready != kube_statefulset_status_replicas"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes StatefulSet replicas mismatch (instance {{ $labels.instance }})
+ description: "StatefulSet does not match the expected number of replicas.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDeploymentGenerationMismatch
+ expr: "kube_deployment_status_observed_generation != kube_deployment_metadata_generation"
+ for: 10m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes Deployment generation mismatch ({{ $labels.namespace }}/{{ $labels.deployment }})
+ description: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} has failed but has not been rolled back.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetGenerationMismatch
+ expr: "kube_statefulset_status_observed_generation != kube_statefulset_metadata_generation"
+ for: 10m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes StatefulSet generation mismatch ({{ $labels.namespace }}/{{ $labels.statefulset }})
+ description: "StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} has failed but has not been rolled back.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesStatefulsetUpdateNotRolledOut
+ expr: "max without (revision) (kube_statefulset_status_current_revision unless kube_statefulset_status_update_revision) * (kube_statefulset_replicas != kube_statefulset_status_replicas_updated)"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes StatefulSet update not rolled out ({{ $labels.namespace }}/{{ $labels.statefulset }})
+ description: "StatefulSet {{ $labels.namespace }}/{{ $labels.statefulset }} update has not been rolled out.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDaemonsetRolloutStuck
+ expr: "kube_daemonset_status_number_ready / kube_daemonset_status_desired_number_scheduled * 100 < 100 or kube_daemonset_status_desired_number_scheduled - kube_daemonset_status_current_number_scheduled > 0"
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes DaemonSet rollout stuck ({{ $labels.namespace }}/{{ $labels.daemonset }})
+ description: "Some Pods of DaemonSet {{ $labels.namespace }}/{{ $labels.daemonset }} are not scheduled or not ready\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesDaemonsetMisscheduled
+ expr: "kube_daemonset_status_number_misscheduled > 0"
+ for: 1m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes DaemonSet misscheduled ({{ $labels.namespace }}/{{ $labels.daemonset }})
+ description: "Some Pods of DaemonSet {{ $labels.namespace }}/{{ $labels.daemonset }} are running where they are not supposed to run\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesCronjobTooLong
+ expr: "time() - kube_cronjob_next_schedule_time > 3600"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes CronJob too long ({{ $labels.namespace }}/{{ $labels.cronjob }})
+ description: "CronJob {{ $labels.namespace }}/{{ $labels.cronjob }} is taking more than 1h to complete.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesJobSlowCompletion
+ expr: "kube_job_spec_completions - kube_job_status_succeeded - kube_job_status_failed > 0"
+ for: 12h
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes job slow completion ({{ $labels.namespace }}/{{ $labels.job_name }})
+ description: "Kubernetes Job {{ $labels.namespace }}/{{ $labels.job_name }} did not complete in time.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesApiServerErrors
+ expr: 'sum(rate(apiserver_request_total{job="apiserver",code=~"(?:5..)"}[1m])) by (instance, job) / sum(rate(apiserver_request_total{job="apiserver"}[1m])) by (instance, job) * 100 > 3'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes API server errors (instance {{ $labels.instance }})
+ description: "Kubernetes API server is experiencing high error rate\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesApiClientErrors
+ expr: '(sum(rate(rest_client_requests_total{code=~"(4|5).."}[1m])) by (instance, job) / sum(rate(rest_client_requests_total[1m])) by (instance, job)) * 100 > 1'
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes API client errors (instance {{ $labels.instance }})
+ description: "Kubernetes API client is experiencing high error rate\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesClientCertificateExpiresNextWeek
+ expr: 'apiserver_client_certificate_expiration_seconds_count{job="apiserver"} > 0 and histogram_quantile(0.01, sum by (job, le) (rate(apiserver_client_certificate_expiration_seconds_bucket{job="apiserver"}[5m]))) < 7*24*60*60'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes client certificate expires next week (instance {{ $labels.instance }})
+ description: "A client certificate used to authenticate to the apiserver is expiring next week.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesClientCertificateExpiresSoon
+ expr: 'apiserver_client_certificate_expiration_seconds_count{job="apiserver"} > 0 and histogram_quantile(0.01, sum by (job, le) (rate(apiserver_client_certificate_expiration_seconds_bucket{job="apiserver"}[5m]))) < 24*60*60'
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Kubernetes client certificate expires soon (instance {{ $labels.instance }})
+ description: "A client certificate used to authenticate to the apiserver is expiring in less than 24.0 hours.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: KubernetesApiServerLatency
+ expr: 'histogram_quantile(0.99, sum(rate(apiserver_request_duration_seconds_bucket{verb!~"(?:CONNECT|WATCHLIST|WATCH|PROXY)"} [10m])) WITHOUT (subresource)) > 1'
+ for: 2m
+ labels:
+ severity: warning
+ annotations:
+ summary: Kubernetes API server latency (instance {{ $labels.instance }})
+ description: "Kubernetes API server has a 99th percentile latency of {{ $value }} seconds for {{ $labels.verb }} {{ $labels.resource }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - name: CoreDNS
+ rules:
+ - alert: CorednsPanicCount
+ expr: increase(coredns_panics_total[1m]) > 0
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: CoreDNS Panic Count (instance {{ $labels.instance }})
+ description: "Number of CoreDNS panics encountered\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - name: PrometheusSelfMonitoring
+ rules:
+ - alert: PrometheusJobMissing
+ expr: 'absent(up{job="prometheus"})'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus job missing (instance {{ $labels.instance }})
+ description: "A Prometheus job has disappeared\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetMissing
+ expr: "up == 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus target missing (instance {{ $labels.instance }})
+ description: "A Prometheus target has disappeared. An exporter might be crashed.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAllTargetsMissing
+ expr: "sum by (job) (up) == 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus all targets missing (instance {{ $labels.instance }})
+ description: "A Prometheus job does not have living target anymore.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetMissingWithWarmupTime
+ expr: "sum by (instance, job) ((up == 0) * on (instance) group_right(job) (node_time_seconds - node_boot_time_seconds > 600))"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus target missing with warmup time (instance {{ $labels.instance }})
+ description: "Allow a job time to start up (10 minutes) before alerting that it's down.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusConfigurationReloadFailure
+ expr: "prometheus_config_last_reload_successful != 1"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus configuration reload failure (instance {{ $labels.instance }})
+ description: "Prometheus configuration reload error\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTooManyRestarts
+ expr: 'changes(process_start_time_seconds{job=~"prometheus|pushgateway|alertmanager"}[15m]) > 2'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus too many restarts (instance {{ $labels.instance }})
+ description: "Prometheus has restarted more than twice in the last 15 minutes. It might be crashlooping.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerJobMissing
+ expr: 'absent(up{job="alertmanager"})'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus AlertManager job missing (instance {{ $labels.instance }})
+ description: "A Prometheus AlertManager job has disappeared\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerConfigurationReloadFailure
+ expr: "alertmanager_config_last_reload_successful != 1"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus AlertManager configuration reload failure (instance {{ $labels.instance }})
+ description: "AlertManager configuration reload error\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerConfigNotSynced
+ expr: 'count(count_values("config_hash", alertmanager_config_hash)) > 1'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus AlertManager config not synced (instance {{ $labels.instance }})
+ description: "Configurations of AlertManager cluster instances are out of sync\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerE2eDeadManSwitch
+ expr: "vector(1)"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus AlertManager E2E dead man switch (instance {{ $labels.instance }})
+ description: "Prometheus DeadManSwitch is an always-firing alert. It's used as an end-to-end test of Prometheus through the Alertmanager.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusNotConnectedToAlertmanager
+ expr: "prometheus_notifications_alertmanagers_discovered < 1"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus not connected to alertmanager (instance {{ $labels.instance }})
+ description: "Prometheus cannot connect the alertmanager\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusRuleEvaluationFailures
+ expr: "increase(prometheus_rule_evaluation_failures_total[3m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus rule evaluation failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} rule evaluation failures, leading to potentially ignored alerts.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTemplateTextExpansionFailures
+ expr: "increase(prometheus_template_text_expansion_failures_total[3m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus template text expansion failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} template text expansion failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusRuleEvaluationSlow
+ expr: "prometheus_rule_group_last_duration_seconds > prometheus_rule_group_interval_seconds"
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus rule evaluation slow (instance {{ $labels.instance }})
+ description: "Prometheus rule evaluation took more time than the scheduled interval. It indicates a slower storage backend access or too complex query.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusNotificationsBacklog
+ expr: "min_over_time(prometheus_notifications_queue_length[10m]) > 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus notifications backlog (instance {{ $labels.instance }})
+ description: "The Prometheus notification queue has not been empty for 10 minutes\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusAlertmanagerNotificationFailing
+ expr: "rate(alertmanager_notifications_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus AlertManager notification failing (instance {{ $labels.instance }})
+ description: "Alertmanager is failing sending notifications\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetEmpty
+ expr: "prometheus_sd_discovered_targets == 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus target empty (instance {{ $labels.instance }})
+ description: "Prometheus has no target in service discovery\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetScrapingSlow
+ expr: 'prometheus_target_interval_length_seconds{quantile="0.9"} / on (interval, instance, job) prometheus_target_interval_length_seconds{quantile="0.5"} > 1.05'
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus target scraping slow (instance {{ $labels.instance }})
+ description: "Prometheus is scraping exporters slowly since it exceeded the requested interval time. Your Prometheus server is under-provisioned.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusLargeScrape
+ expr: "increase(prometheus_target_scrapes_exceeded_sample_limit_total[10m]) > 10"
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus large scrape (instance {{ $labels.instance }})
+ description: "Prometheus has many scrapes that exceed the sample limit\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTargetScrapeDuplicate
+ expr: "increase(prometheus_target_scrapes_sample_duplicate_timestamp_total[5m]) > 0"
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus target scrape duplicate (instance {{ $labels.instance }})
+ description: "Prometheus has many samples rejected due to duplicate timestamps but different values\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbCheckpointCreationFailures
+ expr: "increase(prometheus_tsdb_checkpoint_creations_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB checkpoint creation failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} checkpoint creation failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbCheckpointDeletionFailures
+ expr: "increase(prometheus_tsdb_checkpoint_deletions_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB checkpoint deletion failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} checkpoint deletion failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbCompactionsFailed
+ expr: "increase(prometheus_tsdb_compactions_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB compactions failed (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB compactions failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbHeadTruncationsFailed
+ expr: "increase(prometheus_tsdb_head_truncations_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB head truncations failed (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB head truncation failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbReloadFailures
+ expr: "increase(prometheus_tsdb_reloads_failures_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB reload failures (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB reload failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbWalCorruptions
+ expr: "increase(prometheus_tsdb_wal_corruptions_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB WAL corruptions (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB WAL corruptions\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTsdbWalTruncationsFailed
+ expr: "increase(prometheus_tsdb_wal_truncations_failed_total[1m]) > 0"
+ for: 0m
+ labels:
+ severity: critical
+ annotations:
+ summary: Prometheus TSDB WAL truncations failed (instance {{ $labels.instance }})
+ description: "Prometheus encountered {{ $value }} TSDB WAL truncation failures\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
+
+ - alert: PrometheusTimeseriesCardinality
+ expr: 'label_replace(count by(__name__) ({__name__=~".+"}), "name", "$1", "__name__", "(.+)") > 10000'
+ for: 0m
+ labels:
+ severity: warning
+ annotations:
+ summary: Prometheus timeseries cardinality (instance {{ $labels.instance }})
+ description: "The \"{{ $labels.name }}\" timeseries cardinality is getting very high: {{ $value }}\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
diff --git a/k8s/oxn-platform/.helmignore b/k8s/oxn-platform/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/k8s/oxn-platform/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/k8s/oxn-platform/Chart.yaml b/k8s/oxn-platform/Chart.yaml
new file mode 100644
index 0000000..a03c9bc
--- /dev/null
+++ b/k8s/oxn-platform/Chart.yaml
@@ -0,0 +1,17 @@
+apiVersion: v2
+name: oxn-platform
+description: Umbrella chart for OXN platform
+type: application
+version: 1.0.0
+
+dependencies:
+ - name: frontend-chart
+ version: "latest"
+ appVersion: 0.0.1
+ repository: "file://./charts/frontend-chart"
+# - name: backend
+# version: 1.0.0
+# repository: "file://./charts/backend-chart"
+# - name: analysis
+# version: 1.0.0
+# repository: "file://./charts/analysis-chart"
diff --git a/k8s/oxn-platform/charts/frontend-chart/.helmignore b/k8s/oxn-platform/charts/frontend-chart/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/k8s/oxn-platform/charts/frontend-chart/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/k8s/oxn-platform/charts/frontend-chart/Chart.yaml b/k8s/oxn-platform/charts/frontend-chart/Chart.yaml
new file mode 100644
index 0000000..67c40eb
--- /dev/null
+++ b/k8s/oxn-platform/charts/frontend-chart/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: frontend-chart
+description: A Helm chart for Kubernetes
+type: application
+version: 0.1.0
+appVersion: 0.0.1
\ No newline at end of file
diff --git a/k8s/oxn-platform/charts/frontend-chart/templates/_helpers.tpl b/k8s/oxn-platform/charts/frontend-chart/templates/_helpers.tpl
new file mode 100644
index 0000000..e79d682
--- /dev/null
+++ b/k8s/oxn-platform/charts/frontend-chart/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "frontend-chart.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "frontend-chart.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "frontend-chart.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "frontend-chart.labels" -}}
+helm.sh/chart: {{ include "frontend-chart.chart" . }}
+{{ include "frontend-chart.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "frontend-chart.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "frontend-chart.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "frontend-chart.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "frontend-chart.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/k8s/oxn-platform/charts/frontend-chart/templates/deployment.yaml b/k8s/oxn-platform/charts/frontend-chart/templates/deployment.yaml
new file mode 100644
index 0000000..46d1e93
--- /dev/null
+++ b/k8s/oxn-platform/charts/frontend-chart/templates/deployment.yaml
@@ -0,0 +1,22 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .Chart.Name }}
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app: {{ .Chart.Name }}
+spec:
+ replicas: {{ .Values.replicas }}
+ selector:
+ matchLabels:
+ app: {{ .Chart.Name }}
+ template:
+ metadata:
+ labels:
+ app: {{ .Chart.Name }}
+ spec:
+ containers:
+ - name: {{ .Chart.Name }}
+ image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
+ ports:
+ - containerPort: 3000
diff --git a/k8s/oxn-platform/charts/frontend-chart/templates/service.yaml b/k8s/oxn-platform/charts/frontend-chart/templates/service.yaml
new file mode 100644
index 0000000..fafbde9
--- /dev/null
+++ b/k8s/oxn-platform/charts/frontend-chart/templates/service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ namespace: {{ .Values.global.namespace }}
+spec:
+ selector:
+ app: {{ .Chart.Name }}
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 3000
diff --git a/k8s/oxn-platform/charts/frontend-chart/values.yaml b/k8s/oxn-platform/charts/frontend-chart/values.yaml
new file mode 100644
index 0000000..50f7256
--- /dev/null
+++ b/k8s/oxn-platform/charts/frontend-chart/values.yaml
@@ -0,0 +1,4 @@
+replicas: 1
+image:
+ repository: morauen/oxn-frontend
+ tag: latest
diff --git a/k8s/oxn-platform/templates/_helpers.tpl b/k8s/oxn-platform/templates/_helpers.tpl
new file mode 100644
index 0000000..e1df7b0
--- /dev/null
+++ b/k8s/oxn-platform/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "oxn-platform.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "oxn-platform.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "oxn-platform.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "oxn-platform.labels" -}}
+helm.sh/chart: {{ include "oxn-platform.chart" . }}
+{{ include "oxn-platform.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "oxn-platform.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "oxn-platform.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "oxn-platform.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "oxn-platform.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/k8s/oxn-platform/values.yaml b/k8s/oxn-platform/values.yaml
new file mode 100644
index 0000000..e130c86
--- /dev/null
+++ b/k8s/oxn-platform/values.yaml
@@ -0,0 +1,2 @@
+global:
+ namespace: oxn
diff --git a/k8s/scripts/.gitignore b/k8s/scripts/.gitignore
new file mode 100644
index 0000000..b0c1a52
--- /dev/null
+++ b/k8s/scripts/.gitignore
@@ -0,0 +1,4 @@
+*.log
+*.config
+ig_specs.yaml
+*.hdf5
diff --git a/k8s/scripts/README.md b/k8s/scripts/README.md
new file mode 100644
index 0000000..c38af92
--- /dev/null
+++ b/k8s/scripts/README.md
@@ -0,0 +1,114 @@
+# Kubernetes Cluster Management Scripts
+
+These scripts manage a Kubernetes cluster and install the OpenTelemetry Demo.
+
+## Prerequisites
+
+- Google Cloud SDK
+- Terraform
+- kubectl
+- kOps
+- Helm >= 3.0
+- GCP Project
+
+## Scripts Overview
+
+### Cluster Management
+- `setup.sh`: Enables required GCP APIs for the project
+- `up-cluster.sh `: Creates GKE cluster with spot instances for cost optimization
+- `down-cluster.sh`: Deletes the cluster and cleans up resources including the kOps state store
+
+### OXN Installation and Management
+- `install-oxn.sh`: Sets up the complete environment:
+ - Installs OpenEBS for storage
+ - Deploys Prometheus Stack for monitoring
+ - Installs Kepler for energy metrics
+ - Deploys OpenTelemetry Demo as the system under test
+ - Installs OXN on the control plane node
+
+ Note: be patient, this takes a while (5+ minutes)
+- `uninstall-oxn.sh`: Removes all installed components from the cluster
+- `update-oxn.sh`: Updates the OXN installation on the control plane node with new source code
+
+### Experiment Management
+- `run-experiment.sh [additional oxn arguments]`:
+ - Copies experiment configuration to the control plane
+ - Executes the experiment using OXN
+ - Example: `./run-experiment.sh my-experiment.yaml --logfile test.log --loglevel info`
+
+- `extract-results.sh `:
+ - Copies experiment results from the control plane to your local machine
+ - Example: `./extract-results.sh /opt/oxn/results ./local-results`
+
+## Typical Workflow
+
+1. Enable GCP APIs:
+ ```bash
+ ./setup.sh
+ ```
+
+2. Create the cluster:
+ ```bash
+ ./up-cluster.sh your-project-id
+ ```
+
+3. Install OXN and dependencies:
+ ```bash
+ ./install-oxn.sh
+ ```
+
+4. Run experiments:
+ ```bash
+ ./run-experiment.sh path/to/experiment.yaml
+ ```
+
+5. Extract results:
+ ```bash
+ ./extract-results.sh /opt/oxn/results ./my-results
+ ```
+
+6. When finished, clean up:
+ ```bash
+ ./uninstall-oxn.sh
+ ./down-cluster.sh
+ ```
+
+# Development
+Here are some useful commands for development.
+
+### Accessing graphana:
+```bash
+kubectl port-forward -n oxn-external-monitoring svc/kube-prometheus-grafana 3000:80
+```
+then visit http://localhost:3000/
+user : admin
+password : admin
+
+### Accessing Prometheus (oxn external monitoring)
+```bash
+kubectl port-forward -n oxn-external-monitoring svc/kube-prometheus-kube-prome-prometheus 9091:9090
+```
+then visit http://localhost:9091/
+
+### Accessing the OpenTelemetry Demo:
+```bash
+kubectl port-forward -n system-under-evaluation svc/astronomy-shop-frontendproxy 8080:8080
+```
+then visit http://localhost:8080/
+
+Jaeger: http://localhost:8080/jaeger/ui/
+
+### Accessing the Prometheus Server inside the SUE:
+```bash
+kubectl port-forward -n system-under-evaluation svc/astronomy-shop-prometheus-server 9090:9090
+```
+then visit http://localhost:9090/
+
+
+### To upgrade the Helm chart with new values for the OpenTelemetry Demo:
+```bash
+# from oxn/k8s/
+helm upgrade astronomy-shop open-telemetry/opentelemetry-demo \
+ --namespace system-under-evaluation \
+ -f "manifests/values_opentelemetry_demo.yaml"
+```
diff --git a/k8s/scripts/config/.cluster-config.sh b/k8s/scripts/config/.cluster-config.sh
new file mode 100644
index 0000000..31a1088
--- /dev/null
+++ b/k8s/scripts/config/.cluster-config.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Get the directory where the script is located
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+CONFIG_DIR="${SCRIPT_DIR}/../config"
+
+# Function to generate a random alphabetic prefix
+generate_random_prefix() {
+ LC_ALL=C tr -dc 'a-z' < /dev/urandom | fold -w 6 | head -n 1
+}
+
+# Generate the cluster name only if the file does not exist
+CLUSTER_NAME_FILE="${CONFIG_DIR}/.cluster-name"
+if [ ! -f "$CLUSTER_NAME_FILE" ]; then
+ mkdir -p "${CONFIG_DIR}"
+ PREFIX=$(generate_random_prefix)
+ CLUSTER_NAME="${PREFIX}.oxn.dev.com"
+ echo "$CLUSTER_NAME" > "$CLUSTER_NAME_FILE"
+else
+ # Read the stored cluster name
+ CLUSTER_NAME=$(cat "$CLUSTER_NAME_FILE")
+fi
+
+# Export the cluster name for use in other scripts
+export CLUSTER_NAME
diff --git a/k8s/scripts/config/.cluster-name b/k8s/scripts/config/.cluster-name
new file mode 100644
index 0000000..5f5d9a3
--- /dev/null
+++ b/k8s/scripts/config/.cluster-name
@@ -0,0 +1 @@
+ivqjua.oxn.dev.com
diff --git a/k8s/scripts/debugging/available_metrics.txt b/k8s/scripts/debugging/available_metrics.txt
new file mode 100644
index 0000000..e19c32f
--- /dev/null
+++ b/k8s/scripts/debugging/available_metrics.txt
@@ -0,0 +1,740 @@
+# Available Prometheus Metrics
+
+coredns_build_info
+coredns_cache_entries
+coredns_cache_hits_total
+coredns_cache_misses_total
+coredns_cache_requests_total
+coredns_dns_request_duration_seconds_bucket
+coredns_dns_request_duration_seconds_count
+coredns_dns_request_duration_seconds_sum
+coredns_dns_request_size_bytes_bucket
+coredns_dns_request_size_bytes_count
+coredns_dns_request_size_bytes_sum
+coredns_dns_requests_total
+coredns_dns_response_size_bytes_bucket
+coredns_dns_response_size_bytes_count
+coredns_dns_response_size_bytes_sum
+coredns_dns_responses_total
+coredns_forward_healthcheck_broken_total
+coredns_forward_max_concurrent_rejects_total
+coredns_health_request_duration_seconds_bucket
+coredns_health_request_duration_seconds_count
+coredns_health_request_duration_seconds_sum
+coredns_health_request_failures_total
+coredns_hosts_entries
+coredns_hosts_reload_timestamp_seconds
+coredns_kubernetes_dns_programming_duration_seconds_bucket
+coredns_kubernetes_dns_programming_duration_seconds_count
+coredns_kubernetes_dns_programming_duration_seconds_sum
+coredns_kubernetes_rest_client_rate_limiter_duration_seconds_bucket
+coredns_kubernetes_rest_client_rate_limiter_duration_seconds_count
+coredns_kubernetes_rest_client_rate_limiter_duration_seconds_sum
+coredns_kubernetes_rest_client_request_duration_seconds_bucket
+coredns_kubernetes_rest_client_request_duration_seconds_count
+coredns_kubernetes_rest_client_request_duration_seconds_sum
+coredns_kubernetes_rest_client_requests_total
+coredns_local_localhost_requests_total
+coredns_panics_total
+coredns_plugin_enabled
+coredns_proxy_conn_cache_hits_total
+coredns_proxy_conn_cache_misses_total
+coredns_proxy_request_duration_seconds_bucket
+coredns_proxy_request_duration_seconds_count
+coredns_proxy_request_duration_seconds_sum
+coredns_reload_failed_total
+go_gc_duration_seconds
+go_gc_duration_seconds_count
+go_gc_duration_seconds_sum
+go_goroutines
+go_info
+go_memstats_alloc_bytes
+go_memstats_alloc_bytes_total
+go_memstats_buck_hash_sys_bytes
+go_memstats_frees_total
+go_memstats_gc_sys_bytes
+go_memstats_heap_alloc_bytes
+go_memstats_heap_idle_bytes
+go_memstats_heap_inuse_bytes
+go_memstats_heap_objects
+go_memstats_heap_released_bytes
+go_memstats_heap_sys_bytes
+go_memstats_last_gc_time_seconds
+go_memstats_lookups_total
+go_memstats_mallocs_total
+go_memstats_mcache_inuse_bytes
+go_memstats_mcache_sys_bytes
+go_memstats_mspan_inuse_bytes
+go_memstats_mspan_sys_bytes
+go_memstats_next_gc_bytes
+go_memstats_other_sys_bytes
+go_memstats_stack_inuse_bytes
+go_memstats_stack_sys_bytes
+go_memstats_sys_bytes
+go_threads
+http_client_duration_milliseconds_bucket
+http_client_duration_milliseconds_count
+http_client_duration_milliseconds_sum
+http_server_active_requests
+http_server_duration_milliseconds_bucket
+http_server_duration_milliseconds_count
+http_server_duration_milliseconds_sum
+httpcheck_duration_milliseconds
+httpcheck_error
+httpcheck_status
+jvm_class_count
+jvm_class_loaded_total
+jvm_class_unloaded_total
+jvm_cpu_count
+jvm_cpu_recent_utilization_ratio
+jvm_cpu_time_seconds_total
+jvm_gc_duration_seconds_bucket
+jvm_gc_duration_seconds_count
+jvm_gc_duration_seconds_sum
+jvm_memory_committed_bytes
+jvm_memory_limit_bytes
+jvm_memory_used_after_last_gc_bytes
+jvm_memory_used_bytes
+jvm_thread_count
+kafka_consumer_assigned_partitions
+kafka_consumer_bytes_consumed_rate
+kafka_consumer_bytes_consumed_total
+kafka_consumer_commit_latency_avg
+kafka_consumer_commit_latency_max
+kafka_consumer_commit_rate
+kafka_consumer_commit_sync_time_ns_total
+kafka_consumer_commit_total
+kafka_consumer_committed_time_ns_total
+kafka_consumer_connection_close_rate
+kafka_consumer_connection_close_total
+kafka_consumer_connection_count
+kafka_consumer_connection_creation_rate
+kafka_consumer_connection_creation_total
+kafka_consumer_failed_authentication_rate
+kafka_consumer_failed_authentication_total
+kafka_consumer_failed_reauthentication_rate
+kafka_consumer_failed_reauthentication_total
+kafka_consumer_failed_rebalance_rate_per_hour
+kafka_consumer_failed_rebalance_total
+kafka_consumer_fetch_latency_avg
+kafka_consumer_fetch_latency_max
+kafka_consumer_fetch_rate
+kafka_consumer_fetch_size_avg
+kafka_consumer_fetch_size_max
+kafka_consumer_fetch_throttle_time_avg
+kafka_consumer_fetch_throttle_time_max
+kafka_consumer_fetch_total
+kafka_consumer_heartbeat_rate
+kafka_consumer_heartbeat_response_time_max
+kafka_consumer_heartbeat_total
+kafka_consumer_incoming_byte_rate
+kafka_consumer_incoming_byte_total
+kafka_consumer_io_ratio
+kafka_consumer_io_time_ns_avg
+kafka_consumer_io_time_ns_total
+kafka_consumer_io_wait_ratio
+kafka_consumer_io_wait_time_ns_avg
+kafka_consumer_io_wait_time_ns_total
+kafka_consumer_io_waittime_total
+kafka_consumer_iotime_total
+kafka_consumer_join_rate
+kafka_consumer_join_time_avg
+kafka_consumer_join_time_max
+kafka_consumer_join_total
+kafka_consumer_last_heartbeat_seconds_ago
+kafka_consumer_last_poll_seconds_ago
+kafka_consumer_last_rebalance_seconds_ago
+kafka_consumer_network_io_rate
+kafka_consumer_network_io_total
+kafka_consumer_outgoing_byte_rate
+kafka_consumer_outgoing_byte_total
+kafka_consumer_partition_assigned_latency_avg
+kafka_consumer_partition_assigned_latency_max
+kafka_consumer_poll_idle_ratio_avg
+kafka_consumer_rebalance_latency_avg
+kafka_consumer_rebalance_latency_max
+kafka_consumer_rebalance_latency_total
+kafka_consumer_rebalance_rate_per_hour
+kafka_consumer_rebalance_total
+kafka_consumer_records_consumed_rate
+kafka_consumer_records_consumed_total
+kafka_consumer_records_lag
+kafka_consumer_records_lag_avg
+kafka_consumer_records_lag_max
+kafka_consumer_records_lead
+kafka_consumer_records_lead_avg
+kafka_consumer_records_lead_min
+kafka_consumer_records_per_request_avg
+kafka_consumer_request_rate
+kafka_consumer_request_size_avg
+kafka_consumer_request_size_max
+kafka_consumer_request_total
+kafka_consumer_response_rate
+kafka_consumer_response_total
+kafka_consumer_select_rate
+kafka_consumer_select_total
+kafka_consumer_successful_authentication_no_reauth_total
+kafka_consumer_successful_authentication_rate
+kafka_consumer_successful_authentication_total
+kafka_consumer_successful_reauthentication_rate
+kafka_consumer_successful_reauthentication_total
+kafka_consumer_sync_rate
+kafka_consumer_sync_time_avg
+kafka_consumer_sync_time_max
+kafka_consumer_sync_total
+kafka_consumer_time_between_poll_avg
+kafka_consumer_time_between_poll_max
+kafka_controller_active_count
+kafka_isr_operation_count
+kafka_lag_max
+kafka_logs_flush_Count_milliseconds_total
+kafka_logs_flush_time_50p_milliseconds
+kafka_logs_flush_time_99p_milliseconds
+kafka_message_count_total
+kafka_network_io_bytes_total
+kafka_partition_count
+kafka_partition_offline
+kafka_partition_underReplicated
+kafka_purgatory_size
+kafka_request_count_total
+kafka_request_failed_total
+kafka_request_queue
+kafka_request_time_50p_milliseconds
+kafka_request_time_99p_milliseconds
+kafka_request_time_milliseconds_total
+kube_configmap_created
+kube_configmap_info
+kube_configmap_metadata_resource_version
+kube_daemonset_created
+kube_daemonset_metadata_generation
+kube_daemonset_status_current_number_scheduled
+kube_daemonset_status_desired_number_scheduled
+kube_daemonset_status_number_available
+kube_daemonset_status_number_misscheduled
+kube_daemonset_status_number_ready
+kube_daemonset_status_number_unavailable
+kube_daemonset_status_observed_generation
+kube_daemonset_status_updated_number_scheduled
+kube_deployment_created
+kube_deployment_metadata_generation
+kube_deployment_spec_paused
+kube_deployment_spec_replicas
+kube_deployment_spec_strategy_rollingupdate_max_surge
+kube_deployment_spec_strategy_rollingupdate_max_unavailable
+kube_deployment_status_condition
+kube_deployment_status_observed_generation
+kube_deployment_status_replicas
+kube_deployment_status_replicas_available
+kube_deployment_status_replicas_ready
+kube_deployment_status_replicas_unavailable
+kube_deployment_status_replicas_updated
+kube_endpoint_address
+kube_endpoint_address_available
+kube_endpoint_address_not_ready
+kube_endpoint_created
+kube_endpoint_info
+kube_endpoint_ports
+kube_lease_owner
+kube_lease_renew_time
+kube_limitrange
+kube_limitrange_created
+kube_mutatingwebhookconfiguration_created
+kube_mutatingwebhookconfiguration_info
+kube_mutatingwebhookconfiguration_metadata_resource_version
+kube_mutatingwebhookconfiguration_webhook_clientconfig_service
+kube_namespace_annotations
+kube_namespace_created
+kube_namespace_labels
+kube_namespace_status_phase
+kube_node_created
+kube_node_info
+kube_node_role
+kube_node_spec_taint
+kube_node_spec_unschedulable
+kube_node_status_addresses
+kube_node_status_allocatable
+kube_node_status_capacity
+kube_node_status_condition
+kube_persistentvolume_capacity_bytes
+kube_persistentvolume_claim_ref
+kube_persistentvolume_created
+kube_persistentvolume_info
+kube_persistentvolume_status_phase
+kube_persistentvolume_volume_mode
+kube_persistentvolumeclaim_access_mode
+kube_persistentvolumeclaim_created
+kube_persistentvolumeclaim_info
+kube_persistentvolumeclaim_resource_requests_storage_bytes
+kube_persistentvolumeclaim_status_phase
+kube_pod_annotations
+kube_pod_container_info
+kube_pod_container_resource_limits
+kube_pod_container_resource_requests
+kube_pod_container_state_started
+kube_pod_container_status_last_terminated_exitcode
+kube_pod_container_status_last_terminated_reason
+kube_pod_container_status_last_terminated_timestamp
+kube_pod_container_status_ready
+kube_pod_container_status_restarts_total
+kube_pod_container_status_running
+kube_pod_container_status_terminated
+kube_pod_container_status_waiting
+kube_pod_container_status_waiting_reason
+kube_pod_created
+kube_pod_info
+kube_pod_init_container_info
+kube_pod_init_container_resource_requests
+kube_pod_init_container_status_ready
+kube_pod_init_container_status_restarts_total
+kube_pod_init_container_status_running
+kube_pod_init_container_status_terminated
+kube_pod_init_container_status_terminated_reason
+kube_pod_init_container_status_waiting
+kube_pod_ips
+kube_pod_labels
+kube_pod_owner
+kube_pod_restart_policy
+kube_pod_scheduler
+kube_pod_service_account
+kube_pod_spec_volumes_persistentvolumeclaims_info
+kube_pod_spec_volumes_persistentvolumeclaims_readonly
+kube_pod_start_time
+kube_pod_status_container_ready_time
+kube_pod_status_initialized_time
+kube_pod_status_phase
+kube_pod_status_qos_class
+kube_pod_status_ready
+kube_pod_status_ready_time
+kube_pod_status_reason
+kube_pod_status_scheduled
+kube_pod_status_scheduled_time
+kube_pod_status_unschedulable
+kube_pod_tolerations
+kube_poddisruptionbudget_created
+kube_poddisruptionbudget_status_current_healthy
+kube_poddisruptionbudget_status_desired_healthy
+kube_poddisruptionbudget_status_expected_pods
+kube_poddisruptionbudget_status_observed_generation
+kube_poddisruptionbudget_status_pod_disruptions_allowed
+kube_replicaset_created
+kube_replicaset_metadata_generation
+kube_replicaset_owner
+kube_replicaset_spec_replicas
+kube_replicaset_status_fully_labeled_replicas
+kube_replicaset_status_observed_generation
+kube_replicaset_status_ready_replicas
+kube_replicaset_status_replicas
+kube_secret_created
+kube_secret_info
+kube_secret_metadata_resource_version
+kube_secret_owner
+kube_secret_type
+kube_service_created
+kube_service_info
+kube_service_spec_type
+kube_statefulset_created
+kube_statefulset_metadata_generation
+kube_statefulset_persistentvolumeclaim_retention_policy
+kube_statefulset_replicas
+kube_statefulset_status_current_revision
+kube_statefulset_status_observed_generation
+kube_statefulset_status_replicas
+kube_statefulset_status_replicas_available
+kube_statefulset_status_replicas_current
+kube_statefulset_status_replicas_ready
+kube_statefulset_status_replicas_updated
+kube_statefulset_status_update_revision
+kube_storageclass_created
+kube_storageclass_info
+kube_validatingwebhookconfiguration_created
+kube_validatingwebhookconfiguration_info
+kube_validatingwebhookconfiguration_metadata_resource_version
+kube_validatingwebhookconfiguration_webhook_clientconfig_service
+node_arp_entries
+node_boot_time_seconds
+node_context_switches_total
+node_cooling_device_cur_state
+node_cooling_device_max_state
+node_cpu_guest_seconds_total
+node_cpu_seconds_total
+node_disk_discard_time_seconds_total
+node_disk_discarded_sectors_total
+node_disk_discards_completed_total
+node_disk_discards_merged_total
+node_disk_flush_requests_time_seconds_total
+node_disk_flush_requests_total
+node_disk_info
+node_disk_io_now
+node_disk_io_time_seconds_total
+node_disk_io_time_weighted_seconds_total
+node_disk_read_bytes_total
+node_disk_read_time_seconds_total
+node_disk_reads_completed_total
+node_disk_reads_merged_total
+node_disk_write_time_seconds_total
+node_disk_writes_completed_total
+node_disk_writes_merged_total
+node_disk_written_bytes_total
+node_dmi_info
+node_entropy_available_bits
+node_entropy_pool_size_bits
+node_exporter_build_info
+node_filefd_allocated
+node_filefd_maximum
+node_filesystem_avail_bytes
+node_filesystem_device_error
+node_filesystem_files
+node_filesystem_files_free
+node_filesystem_free_bytes
+node_filesystem_readonly
+node_filesystem_size_bytes
+node_forks_total
+node_intr_total
+node_load1
+node_load15
+node_load5
+node_memory_Active_anon_bytes
+node_memory_Active_bytes
+node_memory_Active_file_bytes
+node_memory_AnonHugePages_bytes
+node_memory_AnonPages_bytes
+node_memory_Bounce_bytes
+node_memory_Buffers_bytes
+node_memory_Cached_bytes
+node_memory_CommitLimit_bytes
+node_memory_Committed_AS_bytes
+node_memory_DirectMap1G_bytes
+node_memory_DirectMap2M_bytes
+node_memory_DirectMap4k_bytes
+node_memory_Dirty_bytes
+node_memory_FileHugePages_bytes
+node_memory_FilePmdMapped_bytes
+node_memory_HardwareCorrupted_bytes
+node_memory_HugePages_Free
+node_memory_HugePages_Rsvd
+node_memory_HugePages_Surp
+node_memory_HugePages_Total
+node_memory_Hugepagesize_bytes
+node_memory_Hugetlb_bytes
+node_memory_Inactive_anon_bytes
+node_memory_Inactive_bytes
+node_memory_Inactive_file_bytes
+node_memory_KReclaimable_bytes
+node_memory_KernelStack_bytes
+node_memory_Mapped_bytes
+node_memory_MemAvailable_bytes
+node_memory_MemFree_bytes
+node_memory_MemTotal_bytes
+node_memory_Mlocked_bytes
+node_memory_NFS_Unstable_bytes
+node_memory_PageTables_bytes
+node_memory_Percpu_bytes
+node_memory_SReclaimable_bytes
+node_memory_SUnreclaim_bytes
+node_memory_SecPageTables_bytes
+node_memory_ShmemHugePages_bytes
+node_memory_ShmemPmdMapped_bytes
+node_memory_Shmem_bytes
+node_memory_Slab_bytes
+node_memory_SwapCached_bytes
+node_memory_SwapFree_bytes
+node_memory_SwapTotal_bytes
+node_memory_Unaccepted_bytes
+node_memory_Unevictable_bytes
+node_memory_VmallocChunk_bytes
+node_memory_VmallocTotal_bytes
+node_memory_VmallocUsed_bytes
+node_memory_WritebackTmp_bytes
+node_memory_Writeback_bytes
+node_memory_Zswap_bytes
+node_memory_Zswapped_bytes
+node_netstat_Icmp6_InErrors
+node_netstat_Icmp6_InMsgs
+node_netstat_Icmp6_OutMsgs
+node_netstat_Icmp_InErrors
+node_netstat_Icmp_InMsgs
+node_netstat_Icmp_OutMsgs
+node_netstat_Ip6_InOctets
+node_netstat_Ip6_OutOctets
+node_netstat_IpExt_InOctets
+node_netstat_IpExt_OutOctets
+node_netstat_Ip_Forwarding
+node_netstat_TcpExt_ListenDrops
+node_netstat_TcpExt_ListenOverflows
+node_netstat_TcpExt_SyncookiesFailed
+node_netstat_TcpExt_SyncookiesRecv
+node_netstat_TcpExt_SyncookiesSent
+node_netstat_TcpExt_TCPOFOQueue
+node_netstat_TcpExt_TCPSynRetrans
+node_netstat_TcpExt_TCPTimeouts
+node_netstat_Tcp_ActiveOpens
+node_netstat_Tcp_CurrEstab
+node_netstat_Tcp_InErrs
+node_netstat_Tcp_InSegs
+node_netstat_Tcp_OutRsts
+node_netstat_Tcp_OutSegs
+node_netstat_Tcp_PassiveOpens
+node_netstat_Tcp_RetransSegs
+node_netstat_Udp6_InDatagrams
+node_netstat_Udp6_InErrors
+node_netstat_Udp6_NoPorts
+node_netstat_Udp6_OutDatagrams
+node_netstat_Udp6_RcvbufErrors
+node_netstat_Udp6_SndbufErrors
+node_netstat_UdpLite6_InErrors
+node_netstat_UdpLite_InErrors
+node_netstat_Udp_InDatagrams
+node_netstat_Udp_InErrors
+node_netstat_Udp_NoPorts
+node_netstat_Udp_OutDatagrams
+node_netstat_Udp_RcvbufErrors
+node_netstat_Udp_SndbufErrors
+node_network_address_assign_type
+node_network_carrier
+node_network_carrier_changes_total
+node_network_carrier_down_changes_total
+node_network_carrier_up_changes_total
+node_network_device_id
+node_network_dormant
+node_network_flags
+node_network_iface_id
+node_network_iface_link
+node_network_iface_link_mode
+node_network_info
+node_network_mtu_bytes
+node_network_name_assign_type
+node_network_net_dev_group
+node_network_protocol_type
+node_network_receive_bytes_total
+node_network_receive_compressed_total
+node_network_receive_drop_total
+node_network_receive_errs_total
+node_network_receive_fifo_total
+node_network_receive_frame_total
+node_network_receive_multicast_total
+node_network_receive_nohandler_total
+node_network_receive_packets_total
+node_network_speed_bytes
+node_network_transmit_bytes_total
+node_network_transmit_carrier_total
+node_network_transmit_colls_total
+node_network_transmit_compressed_total
+node_network_transmit_drop_total
+node_network_transmit_errs_total
+node_network_transmit_fifo_total
+node_network_transmit_packets_total
+node_network_transmit_queue_length
+node_network_up
+node_nf_conntrack_entries
+node_nf_conntrack_entries_limit
+node_nf_conntrack_stat_drop
+node_nf_conntrack_stat_early_drop
+node_nf_conntrack_stat_found
+node_nf_conntrack_stat_ignore
+node_nf_conntrack_stat_insert
+node_nf_conntrack_stat_insert_failed
+node_nf_conntrack_stat_invalid
+node_nf_conntrack_stat_search_restart
+node_os_info
+node_os_version
+node_pressure_cpu_waiting_seconds_total
+node_pressure_io_stalled_seconds_total
+node_pressure_io_waiting_seconds_total
+node_pressure_memory_stalled_seconds_total
+node_pressure_memory_waiting_seconds_total
+node_procs_blocked
+node_procs_running
+node_schedstat_running_seconds_total
+node_schedstat_timeslices_total
+node_schedstat_waiting_seconds_total
+node_scrape_collector_duration_seconds
+node_scrape_collector_success
+node_selinux_enabled
+node_sockstat_FRAG6_inuse
+node_sockstat_FRAG6_memory
+node_sockstat_FRAG_inuse
+node_sockstat_FRAG_memory
+node_sockstat_RAW6_inuse
+node_sockstat_RAW_inuse
+node_sockstat_TCP6_inuse
+node_sockstat_TCP_alloc
+node_sockstat_TCP_inuse
+node_sockstat_TCP_mem
+node_sockstat_TCP_mem_bytes
+node_sockstat_TCP_orphan
+node_sockstat_TCP_tw
+node_sockstat_UDP6_inuse
+node_sockstat_UDPLITE6_inuse
+node_sockstat_UDPLITE_inuse
+node_sockstat_UDP_inuse
+node_sockstat_UDP_mem
+node_sockstat_UDP_mem_bytes
+node_sockstat_sockets_used
+node_softnet_backlog_len
+node_softnet_cpu_collision_total
+node_softnet_dropped_total
+node_softnet_flow_limit_count_total
+node_softnet_processed_total
+node_softnet_received_rps_total
+node_softnet_times_squeezed_total
+node_textfile_scrape_error
+node_time_clocksource_available_info
+node_time_clocksource_current_info
+node_time_seconds
+node_time_zone_offset_seconds
+node_timex_estimated_error_seconds
+node_timex_frequency_adjustment_ratio
+node_timex_loop_time_constant
+node_timex_maxerror_seconds
+node_timex_offset_seconds
+node_timex_pps_calibration_total
+node_timex_pps_error_total
+node_timex_pps_frequency_hertz
+node_timex_pps_jitter_seconds
+node_timex_pps_jitter_total
+node_timex_pps_shift_seconds
+node_timex_pps_stability_exceeded_total
+node_timex_pps_stability_hertz
+node_timex_status
+node_timex_sync_status
+node_timex_tai_offset_seconds
+node_timex_tick_seconds
+node_udp_queues
+node_uname_info
+node_vmstat_oom_kill
+node_vmstat_pgfault
+node_vmstat_pgmajfault
+node_vmstat_pgpgin
+node_vmstat_pgpgout
+node_vmstat_pswpin
+node_vmstat_pswpout
+otelcol_exporter_queue_capacity
+otelcol_exporter_queue_size
+otelcol_exporter_send_failed_log_records
+otelcol_exporter_send_failed_metric_points
+otelcol_exporter_send_failed_spans
+otelcol_exporter_sent_log_records
+otelcol_exporter_sent_metric_points
+otelcol_exporter_sent_spans
+otelcol_otelsvc_k8s_ip_lookup_miss
+otelcol_otelsvc_k8s_pod_added
+otelcol_otelsvc_k8s_pod_deleted
+otelcol_otelsvc_k8s_pod_table_size
+otelcol_otelsvc_k8s_pod_updated
+otelcol_otelsvc_k8s_replicaset_added
+otelcol_otelsvc_k8s_replicaset_updated
+otelcol_process_cpu_seconds
+otelcol_process_memory_rss
+otelcol_process_runtime_heap_alloc_bytes
+otelcol_process_runtime_total_alloc_bytes
+otelcol_process_runtime_total_sys_memory_bytes
+otelcol_process_uptime
+otelcol_processor_accepted_log_records
+otelcol_processor_accepted_metric_points
+otelcol_processor_accepted_spans
+otelcol_processor_batch_batch_send_size_bucket
+otelcol_processor_batch_batch_send_size_count
+otelcol_processor_batch_batch_send_size_sum
+otelcol_processor_batch_metadata_cardinality
+otelcol_processor_batch_timeout_trigger_send
+otelcol_processor_dropped_log_records
+otelcol_processor_dropped_metric_points
+otelcol_processor_dropped_spans
+otelcol_processor_incoming_items
+otelcol_processor_outgoing_items
+otelcol_processor_probabilistic_sampler_count_traces_sampled
+otelcol_processor_refused_log_records
+otelcol_processor_refused_metric_points
+otelcol_processor_refused_spans
+otelcol_receiver_accepted_log_records
+otelcol_receiver_accepted_metric_points
+otelcol_receiver_accepted_spans
+otelcol_receiver_refused_log_records
+otelcol_receiver_refused_metric_points
+otelcol_receiver_refused_spans
+otelcol_scraper_errored_metric_points
+otelcol_scraper_scraped_metric_points
+otlp_exporter_exported_total
+otlp_exporter_seen_total
+process_cpu_count
+process_cpu_seconds_total
+process_cpu_time_seconds_total
+process_max_fds
+process_memory_usage_bytes
+process_memory_virtual_bytes
+process_open_fds
+process_resident_memory_bytes
+process_runtime_cpython_context_switches_total
+process_runtime_cpython_cpu_time_seconds_total
+process_runtime_cpython_cpu_utilization_ratio
+process_runtime_cpython_gc_count_bytes_total
+process_runtime_cpython_memory_bytes
+process_runtime_cpython_thread_count
+process_runtime_dotnet_assemblies_count
+process_runtime_dotnet_exceptions_count_total
+process_runtime_dotnet_gc_allocations_size_bytes_total
+process_runtime_dotnet_gc_collections_count_total
+process_runtime_dotnet_gc_committed_memory_size_bytes
+process_runtime_dotnet_gc_duration_nanoseconds_total
+process_runtime_dotnet_gc_heap_fragmentation_size_bytes
+process_runtime_dotnet_gc_heap_size_bytes
+process_runtime_dotnet_gc_objects_size_bytes
+process_runtime_dotnet_jit_compilation_time_nanoseconds_total
+process_runtime_dotnet_jit_il_compiled_size_bytes_total
+process_runtime_dotnet_jit_methods_compiled_count_total
+process_runtime_dotnet_monitor_lock_contention_count_total
+process_runtime_dotnet_thread_pool_completed_items_count_total
+process_runtime_dotnet_thread_pool_queue_length
+process_runtime_dotnet_thread_pool_threads_count
+process_runtime_dotnet_timer_count
+process_runtime_go_cgo_calls
+process_runtime_go_gc_count_total
+process_runtime_go_gc_pause_ns_total
+process_runtime_go_goroutines
+process_runtime_go_mem_heap_alloc_bytes
+process_runtime_go_mem_heap_idle_bytes
+process_runtime_go_mem_heap_inuse_bytes
+process_runtime_go_mem_heap_objects
+process_runtime_go_mem_heap_released_bytes
+process_runtime_go_mem_heap_sys_bytes
+process_runtime_go_mem_live_objects
+process_runtime_go_mem_lookups_total
+process_start_time_seconds
+process_thread_count
+process_virtual_memory_bytes
+process_virtual_memory_max_bytes
+processedLogs_total
+processedSpans_total
+promhttp_metric_handler_errors_total
+promhttp_metric_handler_requests_in_flight
+promhttp_metric_handler_requests_total
+queueSize_ratio
+rpc_client_duration_milliseconds_bucket
+rpc_client_duration_milliseconds_count
+rpc_client_duration_milliseconds_sum
+runtime_uptime_milliseconds_total
+scrape_duration_seconds
+scrape_samples_post_metric_relabeling
+scrape_samples_scraped
+scrape_series_added
+system_cpu_time_seconds_total
+system_cpu_utilization_ratio
+system_disk_io_bytes_total
+system_disk_operations_total
+system_disk_time_seconds_total
+system_memory_usage_bytes
+system_memory_utilization_ratio
+system_network_connections
+system_network_dropped_packets_total
+system_network_errors_total
+system_network_io_bytes_total
+system_network_packets_total
+system_swap_usage_pages
+system_swap_utilization_ratio
+system_thread_count
+target_info
+traces_span_metrics_calls_total
+traces_span_metrics_duration_milliseconds_bucket
+traces_span_metrics_duration_milliseconds_count
+traces_span_metrics_duration_milliseconds_sum
+up
diff --git a/k8s/scripts/debugging/check_metrics.py b/k8s/scripts/debugging/check_metrics.py
new file mode 100644
index 0000000..37cf39b
--- /dev/null
+++ b/k8s/scripts/debugging/check_metrics.py
@@ -0,0 +1,351 @@
+import requests
+import json
+import time
+from datetime import datetime, timedelta
+import subprocess
+import sys
+import signal
+import logging
+from collections import defaultdict
+from typing import Dict, List, Tuple
+
+# Configure logging
+logging.basicConfig(
+ level=logging.DEBUG,
+ format='%(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# Configuration
+JAEGER_BASE_URL = "http://localhost:8080/jaeger/ui/api"
+PROMETHEUS_BASE_URL = "http://localhost:9090/api/v1"
+CHECK_INTERVAL = 10 # seconds
+RUNTIME_DURATION = timedelta(minutes=1) # How long to run the script for (None for infinite)
+LOOKBACK_WINDOW = {
+ 'prometheus': timedelta(minutes=5), # How far back to look for Prometheus metrics
+ 'jaeger': timedelta(hours=1) # How far back to look for Jaeger traces
+}
+
+########################### PROMETHEUS ###########################
+# Define metrics to monitor
+PROMETHEUS_METRICS = [
+ # Pod Health Metrics
+ {
+ 'name': 'Pod Status Phases',
+ 'query': 'sum by (phase) (kube_pod_status_phase{namespace="system-under-evaluation"})'
+ },
+ {
+ 'name': 'Pod Restarts',
+ 'query': 'sum(kube_pod_container_status_restarts_total{namespace="system-under-evaluation"})'
+ },
+ {
+ 'name': 'Terminated Pods',
+ 'query': 'sum(kube_pod_container_status_terminated{namespace="system-under-evaluation"})'
+ },
+
+ # Network Performance Metrics
+ {
+ 'name': 'Failed Spans',
+ 'query': 'sum(rate(otelcol_exporter_send_failed_spans[1m]))'
+ },
+ {
+ 'name': 'Network Receive Bytes',
+ 'query': 'sum(rate(node_network_receive_bytes_total[1m])) by (instance)'
+ },
+ {
+ 'name': 'Network Transmit Bytes',
+ 'query': 'sum(rate(node_network_transmit_bytes_total[1m])) by (instance)'
+ },
+
+ # Packet Loss Metrics
+ {
+ 'name': 'Network Drops',
+ 'query': 'sum(rate(node_network_receive_drop_total[1m]) + rate(node_network_transmit_drop_total[1m])) by (instance)'
+ },
+ {
+ 'name': 'Network Errors',
+ 'query': 'sum(rate(node_network_receive_errs_total[1m]) + rate(node_network_transmit_errs_total[1m])) by (instance)'
+ },
+ {
+ 'name': 'TCP Retransmissions',
+ 'query': 'sum(rate(node_netstat_Tcp_RetransSegs[1m])) by (instance)'
+ },
+ {
+ 'name': 'TCP/UDP Errors',
+ 'query': 'sum(rate(node_netstat_Tcp_InErrs[1m]) + rate(node_netstat_Udp_InErrors[1m])) by (instance)'
+ },
+
+ # System Health Metrics
+ {
+ 'name': 'Node Load Average',
+ 'query': 'node_load1'
+ },
+ {
+ 'name': 'Memory Available',
+ 'query': 'node_memory_MemAvailable_bytes'
+ },
+ {
+ 'name': 'CPU Usage',
+ 'query': 'sum(rate(node_cpu_seconds_total{mode!="idle"}[1m])) by (instance)'
+ },
+ {
+ 'name': 'TCP Connections',
+ 'query': 'node_netstat_Tcp_CurrEstab'
+ },
+
+ # latency metrics
+ ################### important : 90s AT LEAST #################
+ {
+ 'name': 'Frontend HTTP Latency (p95)',
+ 'query': 'histogram_quantile(0.95, sum(rate(http_server_duration_milliseconds_bucket{job="opentelemetry-demo/frontend"}[90s])) by (http_method, http_status_code, le))'
+ },
+ {
+ 'name': 'Cart Service HTTP Latency (p95)',
+ 'query': 'histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{job="opentelemetry-demo/cartservice"}[90s])) by (http_route, le)) * 1000'
+ },
+ {
+ 'name': 'Product Catalog RPC Latency (p95)',
+ 'query': 'histogram_quantile(0.95, sum(rate(rpc_server_duration_milliseconds_bucket{job="opentelemetry-demo/productcatalogservice"}[90s])) by (rpc_method, le))'
+ },
+ {
+ 'name': 'Ad Service RPC Client Latency (p95)',
+ 'query': 'histogram_quantile(0.95, sum(rate(rpc_client_duration_milliseconds_bucket{job="opentelemetry-demo/adservice"}[90s])) by (rpc_method, rpc_service, le))'
+ }
+]
+
+########################### JAEGER ###########################
+# services to monitor traces for
+JAEGER_SERVICES = [
+ 'frontend',
+ 'recommendationservice',
+ 'productcatalogservice',
+ 'cartservice',
+ 'checkoutservice',
+ 'currencyservice',
+ 'paymentservice',
+ 'shippingservice'
+]
+
+class MetricsStats:
+ def __init__(self):
+ self.total_checks = defaultdict(int)
+ self.failed_checks = defaultdict(int)
+ self.start_time = datetime.now()
+
+ def record_check(self, name: str, success: bool):
+ self.total_checks[name] += 1
+ if not success:
+ self.failed_checks[name] += 1
+
+ def get_summary(self) -> str:
+ runtime = datetime.now() - self.start_time
+ summary = [
+ f"\nMonitoring Summary (Runtime: {str(runtime).split('.')[0]})",
+ "-" * 50
+ ]
+
+ for name in self.total_checks.keys():
+ total = self.total_checks[name]
+ failed = self.failed_checks[name]
+ success_rate = ((total - failed) / total) * 100 if total > 0 else 0
+ status = "✅" if failed == 0 else "❌"
+ summary.append(
+ f"{name} {status}:\n"
+ f" Total Checks: {total}\n"
+ f" Failed Checks: {failed}\n"
+ f" Success Rate: {success_rate:.1f}%"
+ )
+
+ return "\n".join(summary)
+
+class PortForwarder:
+ def __init__(self):
+ self.processes = []
+
+ def start(self) -> bool:
+ """Setup required port-forwards"""
+ try:
+ prometheus_forward = subprocess.Popen(
+ ["kubectl", "port-forward",
+ "-n", "system-under-evaluation",
+ "svc/astronomy-shop-prometheus-server",
+ "9090:9090"],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+
+ frontend_forward = subprocess.Popen(
+ ["kubectl", "port-forward",
+ "-n", "system-under-evaluation",
+ "svc/astronomy-shop-frontendproxy",
+ "8080:8080"],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+
+ self.processes = [prometheus_forward, frontend_forward]
+ time.sleep(5)
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to setup port-forwards: {e}")
+ self.cleanup()
+ return False
+
+ def cleanup(self):
+ for process in self.processes:
+ try:
+ process.terminate()
+ process.wait(timeout=5)
+ except Exception:
+ try:
+ process.kill()
+ except Exception:
+ pass
+ self.processes = []
+
+def check_prometheus_metric(metric_query: str) -> Tuple[bool, str]:
+ """Query Prometheus and check if we get valid data"""
+ try:
+ end_time = datetime.now()
+ start_time = end_time - timedelta(minutes=5)
+
+ params = {
+ 'query': metric_query,
+ 'start': start_time.timestamp(),
+ 'end': end_time.timestamp(),
+ 'step': '15s'
+ }
+
+ response = requests.get(
+ f"{PROMETHEUS_BASE_URL}/query_range",
+ params=params,
+ timeout=10
+ )
+ response.raise_for_status()
+
+ data = response.json()
+ if data['status'] == 'success' and len(data['data']['result']) > 0:
+ return True, "Data received"
+ return False, "No data points found"
+
+ except requests.exceptions.Timeout:
+ return False, "Request timed out"
+ except requests.exceptions.ConnectionError:
+ return False, "Connection failed"
+ except Exception as e:
+ return False, f"Error: {str(e)}"
+
+def check_jaeger_traces(service_name: str) -> Tuple[bool, str]:
+ """Query Jaeger for traces from a specific service"""
+ try:
+ end_time = int(datetime.now().timestamp() * 1_000_000) # microseconds
+ start_time = int((datetime.now() - timedelta(hours=1)).timestamp() * 1_000_000)
+
+ params = {
+ 'service': service_name,
+ 'limit': 20,
+ 'lookback': '1h',
+ 'start': start_time,
+ 'end': end_time
+ }
+
+ response = requests.get(
+ f"{JAEGER_BASE_URL}/traces",
+ params=params,
+ timeout=10
+ )
+
+ if not response.text.strip():
+ return False, "Empty response from Jaeger"
+
+ try:
+ response.raise_for_status()
+ data = response.json()
+ except json.JSONDecodeError:
+ return False, "Invalid JSON response"
+ except requests.exceptions.HTTPError as e:
+ return False, f"HTTP error: {e}"
+
+ if data and len(data.get('data', [])) > 0:
+ return True, f"Found {len(data['data'])} traces"
+ return False, "No traces found"
+
+ except requests.exceptions.Timeout:
+ return False, "Request timed out"
+ except requests.exceptions.ConnectionError:
+ return False, "Connection failed"
+ except Exception as e:
+ return False, f"Error: {str(e)}"
+
+def check_metrics(stats: MetricsStats):
+ """Run one iteration of metric checks"""
+ logger.debug(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]")
+
+ # Check Prometheus metrics
+ logger.debug("\nChecking Prometheus metrics...")
+ for metric in PROMETHEUS_METRICS:
+ success, message = check_prometheus_metric(metric['query'])
+ stats.record_check(f"Prometheus - {metric['name']}", success)
+ if success:
+ logger.debug(f"✅ {metric['name']}: {message}")
+ else:
+ logger.info(f"❌ {metric['name']}: {message}")
+
+ # Check Jaeger traces
+ logger.debug("\nChecking Jaeger traces...")
+ for service in JAEGER_SERVICES:
+ success, message = check_jaeger_traces(service)
+ stats.record_check(f"Jaeger - {service}", success)
+ if success:
+ logger.debug(f"✅ {service}: {message}")
+ else:
+ logger.info(f"❌ {service}: {message}")
+
+def main():
+ # Flag for graceful shutdown
+ running = True
+ forwarder = PortForwarder()
+
+ def signal_handler(signum, frame):
+ nonlocal running
+ running = False
+ print("\nStopping monitoring...")
+
+ # Setup signal handlers
+ signal.signal(signal.SIGINT, signal_handler)
+ signal.signal(signal.SIGTERM, signal_handler)
+
+ print("Setting up port-forwards...")
+ if not forwarder.start():
+ sys.exit(1)
+
+ stats = MetricsStats()
+ start_time = datetime.now()
+
+ try:
+ print("\nStarting continuous monitoring...")
+ print("Only showing errors (failed checks)...")
+
+ while running:
+ try:
+ # Check if runtime duration has elapsed
+ if RUNTIME_DURATION and (datetime.now() - start_time) >= RUNTIME_DURATION:
+ print("\nRuntime duration reached.")
+ break
+
+ check_metrics(stats)
+ time.sleep(CHECK_INTERVAL)
+
+ except Exception as e:
+ logger.error(f"\nError during metric check: {e}")
+ logger.info(f"Retrying in {CHECK_INTERVAL} seconds...")
+ time.sleep(CHECK_INTERVAL)
+
+ finally:
+ print(stats.get_summary())
+ print("\nCleaning up port-forwards...")
+ forwarder.cleanup()
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/k8s/scripts/debugging/visualize_alerts.py b/k8s/scripts/debugging/visualize_alerts.py
new file mode 100644
index 0000000..c2d2c0d
--- /dev/null
+++ b/k8s/scripts/debugging/visualize_alerts.py
@@ -0,0 +1,225 @@
+import argparse
+import requests
+import matplotlib.pyplot as plt
+import pandas as pd
+from datetime import datetime, timedelta
+import subprocess
+import sys
+import signal
+import logging
+from typing import Dict, List, Tuple
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# Configuration
+PROMETHEUS_BASE_URL = "http://localhost:9090/api/v1"
+SEVERITY_COLORS = {
+ 'critical': 'red',
+ 'warning': 'orange',
+ 'info': 'blue',
+ 'none': 'gray'
+}
+
+class PortForwarder:
+ def __init__(self):
+ self.processes = []
+ signal.signal(signal.SIGINT, self.cleanup)
+ signal.signal(signal.SIGTERM, self.cleanup)
+
+ def start(self) -> bool:
+ """Setup required port-forward"""
+ try:
+ prometheus_forward = subprocess.Popen(
+ ["kubectl", "port-forward",
+ "-n", "system-under-evaluation",
+ "svc/astronomy-shop-prometheus-server",
+ "9090:9090"],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+
+ self.processes = [prometheus_forward]
+ # Give port-forward time to establish
+ logger.info("Setting up port-forward...")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to setup port-forward: {e}")
+ self.cleanup()
+ return False
+
+ def cleanup(self, *args):
+ """Clean up port-forward processes"""
+ for process in self.processes:
+ try:
+ process.terminate()
+ process.wait(timeout=5)
+ except Exception:
+ try:
+ process.kill()
+ except Exception:
+ pass
+ self.processes = []
+
+def get_alert_rules() -> List[Dict]:
+ """Get all configured alert rules from Prometheus"""
+ try:
+ response = requests.get(f"{PROMETHEUS_BASE_URL}/rules", timeout=10)
+ response.raise_for_status()
+
+ rules_data = response.json()
+ alert_rules = []
+
+ for group in rules_data.get('data', {}).get('groups', []):
+ for rule in group.get('rules', []):
+ if rule.get('type') == 'alerting':
+ alert_rules.append({
+ 'name': rule.get('name'),
+ 'severity': rule.get('labels', {}).get('severity', 'none')
+ })
+
+ return alert_rules
+
+ except requests.exceptions.RequestException as e:
+ logger.error(f"Failed to fetch alert rules: {e}")
+ return []
+
+def get_alert_history(start_time: datetime, end_time: datetime) -> pd.DataFrame:
+ """Get alert firing history from Prometheus"""
+ try:
+ params = {
+ 'query': 'ALERTS',
+ 'start': start_time.timestamp(),
+ 'end': end_time.timestamp(),
+ 'step': '1m' # 1 minute resolution
+ }
+
+ response = requests.get(
+ f"{PROMETHEUS_BASE_URL}/query_range",
+ params=params,
+ timeout=10
+ )
+ response.raise_for_status()
+
+ # Convert response to DataFrame
+ data = response.json()
+ alert_data = []
+
+ for result in data.get('data', {}).get('result', []):
+ metric = result['metric']
+ values = result['values']
+
+ for timestamp, value in values:
+ if float(value) > 0: # Alert is firing
+ alert_data.append({
+ 'timestamp': pd.to_datetime(timestamp, unit='s'),
+ 'alertname': metric.get('alertname'),
+ 'severity': metric.get('severity', 'none')
+ })
+
+ return pd.DataFrame(alert_data)
+
+ except requests.exceptions.RequestException as e:
+ logger.error(f"Failed to fetch alert history: {e}")
+ return pd.DataFrame()
+
+def create_timeline(
+ alert_data: pd.DataFrame,
+ start_time: datetime,
+ end_time: datetime,
+ output_file: str = None
+):
+ """Create timeline visualization of alerts"""
+ if alert_data.empty:
+ logger.error("No alert data to visualize")
+ return
+
+ # Setup the plot
+ plt.figure(figsize=(15, 8))
+
+ # Get unique alert names
+ alert_names = alert_data['alertname'].unique()
+
+ # Create timeline bars for each alert
+ for idx, alert_name in enumerate(alert_names):
+ alert_instances = alert_data[alert_data['alertname'] == alert_name]
+
+ for _, instance in alert_instances.iterrows():
+ severity = instance['severity']
+ color = SEVERITY_COLORS.get(severity, 'gray')
+
+ plt.hlines(
+ y=idx,
+ xmin=instance['timestamp'],
+ xmax=instance['timestamp'] + pd.Timedelta(minutes=1),
+ color=color,
+ linewidth=10
+ )
+
+ # Customize the plot
+ plt.yticks(range(len(alert_names)), alert_names)
+ plt.xlabel('Time')
+ plt.ylabel('Alert Name')
+ plt.title('Alert Timeline')
+ plt.grid(True, axis='x', alpha=0.3)
+
+ # Add legend
+ legend_elements = [
+ plt.Line2D([0], [0], color=color, label=severity, linewidth=4)
+ for severity, color in SEVERITY_COLORS.items()
+ ]
+ plt.legend(handles=legend_elements, loc='upper right')
+
+ # Rotate x-axis labels for better readability
+ plt.xticks(rotation=45)
+
+ # Adjust layout
+ plt.tight_layout()
+
+ # Save or display
+ if output_file:
+ plt.savefig(output_file)
+ logger.info(f"Saved visualization to {output_file}")
+ else:
+ plt.show()
+
+def main():
+ parser = argparse.ArgumentParser(description='Visualize Prometheus alert history')
+ parser.add_argument('--hours', type=int, default=24,
+ help='Number of hours of history to show (default: 24)')
+ parser.add_argument('--output', type=str,
+ help='Output file path (default: display plot)')
+ args = parser.parse_args()
+
+ # Calculate time range
+ end_time = datetime.now()
+ start_time = end_time - timedelta(hours=args.hours)
+
+ # Setup port-forward
+ forwarder = PortForwarder()
+ if not forwarder.start():
+ sys.exit(1)
+
+ try:
+ # Get alert rules and history
+ logger.info("Fetching alert rules...")
+ alert_rules = get_alert_rules()
+
+ logger.info("Fetching alert history...")
+ alert_data = get_alert_history(start_time, end_time)
+
+ # Create visualization
+ logger.info("Creating visualization...")
+ create_timeline(alert_data, start_time, end_time, args.output)
+
+ finally:
+ logger.info("Cleaning up port-forward...")
+ forwarder.cleanup()
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/k8s/scripts/down-cluster.sh b/k8s/scripts/down-cluster.sh
new file mode 100755
index 0000000..c2d12d3
--- /dev/null
+++ b/k8s/scripts/down-cluster.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -e
+
+# Load the configuration file to get the CLUSTER_NAME
+source config/.cluster-config.sh
+
+if [ -z "$1" ]; then
+ echo "Error: GCP project ID missing"
+ echo "Usage: $0 "
+ exit 1
+fi
+
+PROJECT_ID="$1"
+
+# Set environment variable for kOps state store
+export KOPS_STATE_STORE="gs://$(terraform output -raw kops_state_store_bucket_name)"
+
+# Delete the cluster using kOps
+kops delete cluster --name "${CLUSTER_NAME}" --yes
+
+# Terraform destroy to remove GCS bucket
+terraform destroy -auto-approve -var="project_id=${PROJECT_ID}"
+
+# Remove kubeconfig file
+rm -f "${CLUSTER_NAME}.config"
\ No newline at end of file
diff --git a/k8s/scripts/extract-results.sh b/k8s/scripts/extract-results.sh
new file mode 100755
index 0000000..4733332
--- /dev/null
+++ b/k8s/scripts/extract-results.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+if [ $# -ne 2 ]; then
+ echo "Usage: $0 "
+ echo "Example: $0 /opt/oxn/results /local/path/to/results"
+ exit 1
+fi
+# experiment report generated in /opt/oxn/
+# example experiment report name : experimentsreport_2024-11-17_15-10-45.yaml
+# store.h5 and trie.pickle
+
+
+REMOTE_PATH="$1"
+LOCAL_PATH="$2"
+CONTROL_PLANE_NODE=$(kubectl get nodes --selector=node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')
+
+mkdir -p "${LOCAL_PATH}"
+gcloud compute scp --recurse "${CONTROL_PLANE_NODE}:${REMOTE_PATH}/*" "${LOCAL_PATH}/"
diff --git a/k8s/scripts/install-oxn.sh b/k8s/scripts/install-oxn.sh
new file mode 100755
index 0000000..5c7768c
--- /dev/null
+++ b/k8s/scripts/install-oxn.sh
@@ -0,0 +1,149 @@
+#!/bin/bash
+set -e
+
+# Load the configuration file to get the CLUSTER_NAME
+source config/.cluster-config.sh
+
+# Check if kubectl is available
+if ! command -v kubectl &> /dev/null; then
+ echo "kubectl is not installed"
+ exit 1
+fi
+
+# Check if helm is available
+if ! command -v helm &> /dev/null; then
+ echo "helm is not installed"
+ exit 1
+fi
+
+# Define directories relative to script location
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+MANIFESTS_DIR="${SCRIPT_DIR}/../manifests"
+DASHBOARDS_DIR="${SCRIPT_DIR}/../dashboards"
+
+# Verify directories exist
+if [ ! -d "$MANIFESTS_DIR" ]; then
+ echo "Error: Manifests directory not found: $MANIFESTS_DIR"
+ exit 1
+fi
+
+if [ ! -d "$DASHBOARDS_DIR" ]; then
+ echo "Error: Dashboards directory not found: $DASHBOARDS_DIR"
+ exit 1
+fi
+
+echo "Installing OpenEBS..."
+kubectl apply -f https://openebs.github.io/charts/openebs-operator.yaml
+
+echo "Installing Prometheus Stack..."
+helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+helm repo update
+helm install kube-prometheus prometheus-community/kube-prometheus-stack \
+ --namespace oxn-external-monitoring \
+ --create-namespace \
+ --version 62.5.1 \
+ -f "${MANIFESTS_DIR}/values_kube_prometheus.yaml"
+
+#echo "Installing Kepler..."
+#helm repo add kepler https://sustainable-computing-io.github.io/kepler-helm-chart
+#helm repo update
+#helm install kepler kepler/kepler \
+# --namespace oxn-external-monitoring \
+# --create-namespace \
+# --set serviceMonitor.enabled=true \
+# --set serviceMonitor.labels.release=kube-prometheus \
+# -f "${MANIFESTS_DIR}/values_kepler.yaml"
+
+echo "Waiting for Grafana pod to be ready..."
+kubectl wait --for=condition=ready pod \
+ -l app.kubernetes.io/name=grafana \
+ -n oxn-external-monitoring \
+ --timeout=300s
+
+#echo "Copying Kepler dashboard to Grafana..."
+#GF_POD=$(kubectl get pod \
+# -n oxn-external-monitoring \
+# -l app.kubernetes.io/name=grafana \
+# -o jsonpath="{.items[0].metadata.name}")
+#kubectl cp "${DASHBOARDS_DIR}/kepler_dashboard.json" \
+# "oxn-external-monitoring/${GF_POD}:/tmp/dashboards/kepler_dashboard.json"
+
+echo "Installing OpenTelemetry Demo..."
+helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
+helm repo update
+helm install astronomy-shop open-telemetry/opentelemetry-demo \
+ --namespace system-under-evaluation \
+ --create-namespace \
+ -f "${MANIFESTS_DIR}/values_opentelemetry_demo.yaml"
+
+echo "Waiting for OpenTelemetry Demo pods to be ready..."
+kubectl wait --for=condition=ready pod \
+ --all \
+ -n system-under-evaluation \
+ --timeout=300s
+
+echo "Copying kubeconfig and OXN source to control plane node..."
+CONTROL_PLANE_NODE=$(kubectl get nodes --selector=node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')
+OXN_SOURCE_DIR="${SCRIPT_DIR}/../.."
+
+# Create directories on control plane node and set permissions
+gcloud compute ssh "${CONTROL_PLANE_NODE}" --command='
+ sudo mkdir -p /opt/oxn ~/.kube
+'
+
+# Copy kubeconfig
+gcloud compute scp "${CLUSTER_NAME}.config" "${CONTROL_PLANE_NODE}:/tmp/kubeconfig"
+gcloud compute ssh "${CONTROL_PLANE_NODE}" --command="sudo mv /tmp/kubeconfig ~/.kube/config"
+
+# Copy OXN source files
+echo "Creating zip archive of source code..."
+(cd "${OXN_SOURCE_DIR}" && zip -r /tmp/oxn-source.zip ./* -x "k8s/scripts/.terraform/*" -x "venv/*")
+gcloud compute scp /tmp/oxn-source.zip "${CONTROL_PLANE_NODE}:/tmp/"
+
+# Install Python and OXN
+gcloud compute ssh "${CONTROL_PLANE_NODE}" --command='
+ # Install Python and requirements
+ sudo apt-get update
+ sudo apt-get install -y python3 python3-pip unzip
+
+ # Setup directories
+ sudo chown -R $(whoami):$(whoami) /opt/oxn
+
+ # Install virtualenv globally to avoid PATH issues
+ sudo pip3 install virtualenv
+
+ # Create virtualenv
+ cd /opt/oxn
+ python3 -m virtualenv venv
+
+ # Extract OXN source
+ unzip -o -q /tmp/oxn-source.zip
+ rm /tmp/oxn-source.zip
+
+ # Install OXN in virtualenv
+ source venv/bin/activate
+ pip3 install .
+
+ # Verify installation
+ which oxn
+ oxn --help
+'
+
+rm -f /tmp/oxn-source.zip
+
+echo "Installation complete!"
+echo "To run an experiment: ./run-experiment.sh [additional oxn arguments]"
+echo "To extract results: ./extract-results.sh "
+
+echo "Installing OXN Platform..."
+kubectl create namespace oxn --dry-run=client -o yaml | kubectl apply -f -
+helm install oxn-platform ../oxn-platform \
+ --namespace oxn \
+ --create-namespace
+
+echo "Waiting for OXN Platform pods to be ready..."
+kubectl wait --for=condition=ready pod \
+ --all \
+ -n oxn \
+ --timeout=300s
+
\ No newline at end of file
diff --git a/k8s/scripts/main.tf b/k8s/scripts/main.tf
new file mode 100644
index 0000000..f459cfd
--- /dev/null
+++ b/k8s/scripts/main.tf
@@ -0,0 +1,37 @@
+variable "project_id" {
+ description = "advanced-cloud-prototyping"
+ type = string
+}
+
+# random suffix
+resource "random_id" "bucket_suffix" {
+ byte_length = 4
+}
+
+
+provider "google" {
+ project = var.project_id
+ region = "europe-west1-b"
+}
+
+# Create a GCS bucket for the kOps state store
+resource "google_storage_bucket" "kops_state_store_oxn" {
+ name = "kops-state-store-${random_id.bucket_suffix.hex}"
+ location = "EU"
+ force_destroy = true # allows bucket to be destroyed without emptying
+
+ lifecycle_rule {
+ action {
+ type = "Delete"
+ }
+ condition {
+ age = 2 # auto-delete after 2 days
+ }
+ }
+}
+
+# output the bucket name for reference
+output "kops_state_store_bucket_name" {
+ value = google_storage_bucket.kops_state_store_oxn.name
+}
+
diff --git a/k8s/scripts/run-experiment.sh b/k8s/scripts/run-experiment.sh
new file mode 100755
index 0000000..1727415
--- /dev/null
+++ b/k8s/scripts/run-experiment.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+if [ $# -lt 1 ]; then
+ echo "Usage: $0 [additional oxn arguments]"
+ echo "Example: $0 my-experiment.yaml --times 3 --report report.yaml"
+ exit 1
+fi
+
+EXPERIMENT_FILE="$1"
+shift # Remove first argument, leaving remaining args for oxn
+
+# Copy experiment file to control plane
+CONTROL_PLANE_NODE=$(kubectl get nodes --selector=node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')
+
+REMOTE_PATH="/tmp/$(basename ${EXPERIMENT_FILE})"
+gcloud compute scp "${EXPERIMENT_FILE}" "${CONTROL_PLANE_NODE}:${REMOTE_PATH}"
+
+# Run experiment with virtualenv activated
+gcloud compute ssh "${CONTROL_PLANE_NODE}" --command="cd /opt/oxn && source venv/bin/activate && oxn ${REMOTE_PATH} $*"
\ No newline at end of file
diff --git a/k8s/scripts/setup.sh b/k8s/scripts/setup.sh
new file mode 100755
index 0000000..a9c7924
--- /dev/null
+++ b/k8s/scripts/setup.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+# Enable required APIs
+gcloud services enable cloudresourcemanager.googleapis.com
+gcloud services enable iam.googleapis.com
+gcloud services enable container.googleapis.com
+gcloud services enable storage.googleapis.com
diff --git a/k8s/scripts/uninstall-oxn.sh b/k8s/scripts/uninstall-oxn.sh
new file mode 100755
index 0000000..a8770b4
--- /dev/null
+++ b/k8s/scripts/uninstall-oxn.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# Uninstall OpenTelemetry Demo
+echo "Uninstalling OpenTelemetry Demo..."
+helm uninstall astronomy-shop --namespace system-under-evaluation
+kubectl delete namespace system-under-evaluation --ignore-not-found
+
+# Uninstall Kepler
+echo "Uninstalling Kepler..."
+helm uninstall kepler --namespace oxn-external-monitoring
+kubectl delete namespace oxn-external-monitoring --ignore-not-found
+
+# Uninstall Prometheus Stack
+echo "Uninstalling Prometheus Stack..."
+helm uninstall kube-prometheus --namespace oxn-external-monitoring
+
+# Uninstall OpenEBS
+echo "Uninstalling OpenEBS..."
+kubectl delete -f https://openebs.github.io/charts/openebs-operator.yaml --ignore-not-found
+
+# Uninstall OXN Platform
+echo "Uninstalling OXN Platform..."
+helm uninstall oxn-platform --namespace oxn
+kubectl delete namespace oxn --ignore-not-found
+
+# Clean up Helm repos
+echo "Removing Helm repositories..."
+helm repo remove prometheus-community
+helm repo remove kepler
+helm repo remove open-telemetry
+
+echo "Uninstall complete!"
\ No newline at end of file
diff --git a/k8s/scripts/up-cluster.sh b/k8s/scripts/up-cluster.sh
new file mode 100755
index 0000000..1504ab3
--- /dev/null
+++ b/k8s/scripts/up-cluster.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+
+set -e
+
+# Load the configuration file to get the CLUSTER_NAME
+source "config/.cluster-config.sh"
+
+# parameters
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 "
+ echo "Example: $0 my-gcp-project"
+ exit 1
+fi
+
+GCP_PROJECT_ID=$1 # GCP project ID
+ZONE="europe-west1-b" # Single zone since HA is not needed
+NODE_COUNT=3
+CONTROL_PLANE_SIZE="e2-standard-2"
+NODE_SIZE="e2-standard-2"
+
+# Debug output to verify cluster name
+echo "Using cluster name: ${CLUSTER_NAME}"
+
+# create GCS bucket
+terraform apply -var="project_id=${GCP_PROJECT_ID}" -auto-approve
+
+# Get the state store bucket name from Terraform output
+export KOPS_STATE_STORE="gs://$(terraform output -raw kops_state_store_bucket_name)"
+
+# Create the cluster configuration
+echo "Creating cluster configuration..."
+kops create cluster \
+ --name="${CLUSTER_NAME}" \
+ --state="${KOPS_STATE_STORE}" \
+ --zones="${ZONE}" \
+ --control-plane-zones="${ZONE}" \
+ --node-count="${NODE_COUNT}" \
+ --control-plane-size="${CONTROL_PLANE_SIZE}" \
+ --node-size="${NODE_SIZE}" \
+ --control-plane-count=1 \
+ --networking=cilium \
+ --cloud=gce \
+ --project="${GCP_PROJECT_ID}" \
+
+# Get the instance group specs
+echo "Modifying instance groups to use spot instances..."
+kops get ig --name "${CLUSTER_NAME}" -o yaml > ig_specs.yaml
+
+# Modify the instance group specs to use spot instances
+# This adds the gcpProvisioningModel: SPOT to both node and control-plane specs
+# Uncomment this if you want to use spot instances
+# sed -i '/spec:/a\ gcpProvisioningModel: SPOT' ig_specs.yaml
+# kops replace -f ig_specs.yaml
+
+# Create the cluster
+echo "Creating the cluster..."
+kops update cluster --name="${CLUSTER_NAME}" --yes
+
+# Update local kubeconfig
+kops export kubeconfig --admin --kubeconfig="${CLUSTER_NAME}.config"
+kops export kubeconfig --admin
+# Wait for the cluster to be ready
+echo "Waiting for cluster to be ready..."
+kops validate cluster --wait 10m
+
diff --git a/k8s/scripts/update-oxn.sh b/k8s/scripts/update-oxn.sh
new file mode 100755
index 0000000..e1f3d89
--- /dev/null
+++ b/k8s/scripts/update-oxn.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+OXN_SOURCE_DIR="${SCRIPT_DIR}/../.."
+CONTROL_PLANE_NODE=$(kubectl get nodes --selector=node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')
+
+echo "Updating OXN source code on control plane node..."
+
+# Create temp zip file
+echo "Creating zip archive of source code..."
+rm -f /tmp/oxn-source.zip
+(cd "${OXN_SOURCE_DIR}" && zip -r /tmp/oxn-source.zip ./* -x "k8s/scripts/.terraform/*" -x "venv/*")
+
+# Copy zip file and extract on remote
+echo "Copying and extracting source code..."
+gcloud compute scp /tmp/oxn-source.zip "${CONTROL_PLANE_NODE}:/tmp/"
+
+gcloud compute ssh "${CONTROL_PLANE_NODE}" --command='
+ # Clean up old files but preserve virtualenv
+ sudo rm -rf /opt/oxn/*
+ sudo mkdir -p /opt/oxn
+ sudo chown -R $(whoami):$(whoami) /opt/oxn
+
+ # Extract OXN source
+ cd /opt/oxn
+ unzip -q /tmp/oxn-source.zip
+ rm /tmp/oxn-source.zip
+
+ # Install OXN in virtualenv
+ source venv/bin/activate
+ pip3 install .
+
+ # Verify installation
+ which oxn
+ oxn --help
+'
+
+rm -f /tmp/oxn-source.zip
+
+echo "OXN source code updated successfully!"
\ No newline at end of file
diff --git a/locust/locust_otel_demo.py b/locust/locust_otel_demo.py
index 0b20d9e..818df24 100644
--- a/locust/locust_otel_demo.py
+++ b/locust/locust_otel_demo.py
@@ -36,8 +36,165 @@
"HQTGWGPNH4",
]
-people_file = open('locust/people.json')
-people = json.load(people_file)
+people = """[
+ {
+ "email": "larry_sergei@example.com",
+ "address": {
+ "streetAddress": "1600 Amphitheatre Parkway",
+ "zipCode": "94043",
+ "city": "Mountain View",
+ "state": "CA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4432-8015-6152-0454",
+ "creditCardExpirationMonth": 1,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 672
+ }
+ },
+ {
+ "email": "bill@example.com",
+ "address": {
+ "streetAddress": "One Microsoft Way",
+ "zipCode": "98052",
+ "city": "Redmond",
+ "state": "WA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4532-4211-7434-1278",
+ "creditCardExpirationMonth": 2,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 114
+ }
+ },
+ {
+ "email": "steve@example.com",
+ "address": {
+ "streetAddress": "One Apple Park Way",
+ "zipCode": "95014",
+ "city": "Cupertino",
+ "state": "CA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4532-6178-2799-1951",
+ "creditCardExpirationMonth": 3,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 239
+ }
+ },
+ {
+ "email": "mark@example.com",
+ "address": {
+ "streetAddress": "1 Hacker Way",
+ "zipCode": "94025",
+ "city": "Menlo Park",
+ "state": "CA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4539-1103-5661-7083",
+ "creditCardExpirationMonth": 4,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 784
+ }
+ },
+ {
+ "email": "jeff@example.com",
+ "address": {
+ "streetAddress": "410 Terry Ave N",
+ "zipCode": "98109",
+ "city": "Seattle",
+ "state": "WA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4916-0816-6217-7968",
+ "creditCardExpirationMonth": 5,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 397
+ }
+ },
+ {
+ "email": "reed@example.com",
+ "address": {
+ "streetAddress": "100 Winchester Circle",
+ "zipCode": "95032",
+ "city": "Los Gatos",
+ "state": "CA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4929-5431-0337-5647",
+ "creditCardExpirationMonth": 6,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 793
+ }
+ },
+ {
+ "email": "tobias@example.com",
+ "address": {
+ "streetAddress": "150 Elgin St",
+ "zipCode": "K2P1L4",
+ "city": "Ottawa",
+ "state": "ON",
+ "country": "Canada"
+ },
+ "userCurrency": "CAD",
+ "creditCard": {
+ "creditCardNumber": "4763-1844-9699-8031",
+ "creditCardExpirationMonth": 7,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 488
+ }
+ },
+ {
+ "email": "jack@example.com",
+ "address": {
+ "streetAddress": "1355 Market St",
+ "zipCode": "94103",
+ "city": "San Francisco",
+ "state": "CA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4929-6495-8333-3657",
+ "creditCardExpirationMonth": 8,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 159
+ }
+ },
+ {
+ "email": "moore@example.com",
+ "address": {
+ "streetAddress": "2200 Mission College Blvd",
+ "zipCode": "95054",
+ "city": "Santa Clara",
+ "state": "CA",
+ "country": "United States"
+ },
+ "userCurrency": "USD",
+ "creditCard": {
+ "creditCardNumber": "4485-4803-8707-3547",
+ "creditCardExpirationMonth": 9,
+ "creditCardExpirationYear": 2039,
+ "creditCardCvv": 682
+ }
+ }
+]"""
+
+# TODO make this configurable probably?
+#people_file = open('opt/oxn/locust/people.json')
+people = json.loads(people)
class WebsiteUser(HttpUser):
wait_time = between(1, 10)
diff --git a/oxn/argparser.py b/oxn/argparser.py
index 512811b..7ee971d 100644
--- a/oxn/argparser.py
+++ b/oxn/argparser.py
@@ -18,6 +18,17 @@ def validate_file(file):
return file
+def validate_output_formats(formats):
+ valid_formats = {'hdf', 'json'}
+ formats = set(formats.split(','))
+ invalid = formats - valid_formats
+ if invalid:
+ raise argparse.ArgumentTypeError(
+ f"Invalid output format(s): {', '.join(invalid)}. Valid formats are: {', '.join(valid_formats)}"
+ )
+ return formats
+
+
parser = argparse.ArgumentParser(
prog="oxn",
description="Observability experiments engine",
@@ -80,6 +91,21 @@ def validate_file(file):
type=time_string_to_seconds
)
+parser.add_argument(
+ "--out-path",
+ dest="out_path",
+ type=str,
+ help="Directory path where experiment data should be stored. If not specified, data will be stored in the default location.",
+)
+
+parser.add_argument(
+ "--out",
+ dest="out_formats",
+ type=validate_output_formats,
+ default={'hdf'},
+ help="Comma-separated (no spaces) list of output formats. Valid formats are: hdf, json. Default is hdf",
+)
+
def parse_oxn_args(args):
args = parser.parse_args(args)
diff --git a/oxn/engine.py b/oxn/engine.py
index c8bc37d..6717ce7 100644
--- a/oxn/engine.py
+++ b/oxn/engine.py
@@ -6,18 +6,18 @@
"""
import logging
-import schema
import yaml
+from jsonschema import validate
from .runner import ExperimentRunner
from .docker_orchestration import DockerComposeOrchestrator
from .kubernetes_orchestrator import KubernetesOrchestrator
from .report import Reporter
-from .store import write_dataframe
+from .store import configure_output_path, write_dataframe, write_json_data
from .loadgen import LoadGenerator
from .locust_file_loadgenerator import LocustFileLoadgenerator
from .utils import utc_timestamp
-from .validation import syntactic_schema
+from .validation import load_schema
from .context import Context
from .errors import OxnException, OrchestrationException
@@ -31,7 +31,7 @@ class Engine:
This class encapsulates all behavior needed to execute observability experiments.
"""
- def __init__(self, configuration_path=None, report_path=None, treatment_file=None):
+ def __init__(self, configuration_path=None, report_path=None, out_path=None, out_formats=None, treatment_file=None):
assert configuration_path is not None, "Configuration path must be specified"
self.config = configuration_path
"""The path to the configuration file for this engine"""
@@ -42,6 +42,10 @@ def __init__(self, configuration_path=None, report_path=None, treatment_file=Non
assert report_path is not None, "Report path must be specified"
self.reporter = Reporter(report_path=report_path)
"""A reference to a reporter instance"""
+ self.out_path = out_path
+ """The path to write the experiment data to"""
+ self.out_formats = out_formats
+ """The formats to write the experiment data to"""
self.context = Context(treatment_file_path=treatment_file)
"""A reference to a treatment context"""
self.additional_treatments = self.context.load_treatment_file()
@@ -56,6 +60,9 @@ def __init__(self, configuration_path=None, report_path=None, treatment_file=Non
"""Status of the load generator"""
self.sue_running = False
"""Status of the sue"""
+ # configure the output path for HDF storage
+ if self.out_path:
+ configure_output_path(self.out_path)
def read_experiment_specification(self):
"""Read the experiment specification file and confirm that its valid yaml"""
@@ -72,10 +79,12 @@ def read_experiment_specification(self):
def validate_syntax(self):
"""Validate the specification syntactically"""
try:
- syntactic_schema.validate(data=self.spec)
- except schema.SchemaError as e:
+ schema = load_schema()
+ validate(instance=self.spec, schema=schema)
+ except Exception as e:
raise OxnException(
- message="Can't validate experiment spec", explanation=str(e)
+ message="Can't validate experiment spec",
+ explanation=str(e)
)
def run(
@@ -155,12 +164,23 @@ def run(
self.loadgen_running = False
logger.info("Stopped load generation")
for _, response in self.runner.observer.variables().items():
- write_dataframe(
- dataframe=response.data,
- experiment_key=self.runner.config_filename,
- run_key=self.runner.short_id,
- response_key=response.name,
- )
+ # default is hdf
+ if self.out_formats and 'hdf' in self.out_formats:
+ write_dataframe(
+ dataframe=response.data,
+ experiment_key=self.runner.config_filename,
+ run_key=self.runner.short_id,
+ response_key=response.name,
+ )
+ if self.out_formats and 'json' in self.out_formats:
+ write_json_data(
+ data=response.data,
+ experiment_key=self.runner.config_filename,
+ run_key=self.runner.short_id,
+ response_key=response.name,
+ out_path=self.out_path,
+ )
+
logger.debug(
f"Experiment {self.runner.config_filename}: DataFrame: {len(response.data)} rows"
)
diff --git a/oxn/locust_file_loadgenerator.py b/oxn/locust_file_loadgenerator.py
index 5fb0657..0222739 100644
--- a/oxn/locust_file_loadgenerator.py
+++ b/oxn/locust_file_loadgenerator.py
@@ -1,4 +1,3 @@
-
import importlib
from importlib.machinery import ModuleSpec
import importlib.util
@@ -68,7 +67,8 @@ def _read_config(self):
self.env.create_local_runner()
for locust_file in self.locust_files:
- path = locust_file["path"]
+ path = locust_file["path"] if isinstance(locust_file, dict) else locust_file
+
locust_module = self._load_locust_file(path)
for user_class in dir(locust_module):
user_class_instance = getattr(locust_module, user_class)
diff --git a/oxn/main.py b/oxn/main.py
index ea79e19..0e1c732 100644
--- a/oxn/main.py
+++ b/oxn/main.py
@@ -27,6 +27,8 @@ def main():
configuration_path=args.spec,
report_path=args.report,
treatment_file=args.extend,
+ out_path=args.out_path,
+ out_formats=args.out_formats,
)
try:
engine.read_experiment_specification()
diff --git a/oxn/observer.py b/oxn/observer.py
index bc43914..9877e2c 100644
--- a/oxn/observer.py
+++ b/oxn/observer.py
@@ -5,7 +5,7 @@
Module to handle data capture during experiment execution"""
import logging
-from typing import Optional
+from typing import Dict, List, Optional
from operator import attrgetter
from .models.orchestrator import Orchestrator
@@ -24,90 +24,71 @@ class Observer:
an experiment description and then observing the variables during or after an experiment.
"""
- def __init__(
- self,
- orchestrator: Orchestrator,
- config: Optional[dict],
- experiment_start: Optional[float] = None,
- experiment_end: Optional[float] = None,
- ):
- assert orchestrator is not None
- self.orchestrator = orchestrator
- """The orchestrator instance"""
+ def __init__(self, config: dict, orchestrator):
self.config = config
- """The experiment specification"""
- self.experiment_start = experiment_start
- """The experiment start timestamp """
- self.experiment_end = experiment_end
- """The experiment end timestamp"""
- self._response_variables: dict = {}
- """The response variables constructed from the specification and experiment start / end timestamps"""
-
- def _initialize_metric_variable(self, response_name, response_description) -> None:
- response_variable = MetricResponseVariable(
- orchestrator=self.orchestrator,
- name=response_name,
- description=response_description,
- experiment_start=self.experiment_start,
- experiment_end=self.experiment_end,
- target=response_description["target"],
- )
- self._response_variables[response_variable.name] = response_variable
-
- def _initialize_trace_variable(self, response_name, response_description) -> None:
- """Initialize a trace variable from a response description"""
- response_variable = TraceResponseVariable(
- orchestrator=self.orchestrator,
- name=response_name,
- description=response_description,
- experiment_start=self.experiment_start,
- experiment_end=self.experiment_end,
- )
- self._response_variables[response_variable.name] = response_variable
+ self.orchestrator = orchestrator
+ self.experiment_start: Optional[float] = None
+ self.experiment_end: Optional[float] = None
+ self._response_variables: Dict[str, ResponseVariable] = {}
def initialize_variables(self) -> None:
- """
- Process the response variable section from the specification config
+ """Initialize response variables from the experiment specification"""
+ if not self.config or not self.experiment_start or not self.experiment_end:
+ return
- Note that we cannot initialize this straightaway when reading the experiment specification,
- as the observational windows depend on experiment start and end times.
- """
responses = self.config["experiment"]["responses"]
for response in responses:
- for response_name, response_params in response.items():
- response_type = response_params["type"]
- if response_type == "trace":
- self._initialize_trace_variable(
- response_name=response_name,
- response_description=response_params,
- )
- if response_type == "metric":
- self._initialize_metric_variable(
- response_name=response_name,
- response_description=response_params,
- )
-
- def variables(self) -> dict[str, ResponseVariable]:
+ response_type = response["type"]
+ name = response["name"]
+
+ if response_type == "metric":
+ response_variable = MetricResponseVariable(
+ orchestrator=self.orchestrator,
+ name=name,
+ experiment_start=self.experiment_start,
+ experiment_end=self.experiment_end,
+ description=response,
+ target=response["target"],
+ right_window=response["right_window"],
+ left_window=response["left_window"],
+ )
+ self._response_variables[name] = response_variable
+
+ elif response_type == "trace":
+ response_variable = TraceResponseVariable(
+ orchestrator=self.orchestrator,
+ name=name,
+ experiment_start=self.experiment_start,
+ experiment_end=self.experiment_end,
+ description=response,
+ right_window=response["right_window"],
+ left_window=response["left_window"],
+ )
+ self._response_variables[name] = response_variable
+
+ def variables(self) -> Dict[str, ResponseVariable]:
+ """Return all response variables"""
return self._response_variables
def time_to_wait_right(self) -> float:
- """Determine the time to wait before observing the variables"""
- max_right_window = max(self.variables().values(), key=attrgetter("end"))
- diff = max_right_window.end - self.experiment_end
- return diff
-
- def time_to_wait_left(self):
- """
- Determine the time to wait on the left side of an experiment start
- """
- responses = self.config["experiment"]["responses"]
- max_left_window = 0
- for response in responses:
- for response_name, response_params in response.items():
- in_seconds = time_string_to_seconds(response_params["left_window"])
- if in_seconds > max_left_window:
- max_left_window = in_seconds
- return max_left_window
+ """Return the maximum right window time to wait"""
+ max_time = 0
+ for response in self.variables().values():
+ # If we ever implement a response variable that does not have a right window, this will break
+ in_seconds = time_string_to_seconds(response.right_window)
+ if in_seconds > max_time:
+ max_time = in_seconds
+ return max_time
+
+ def time_to_wait_left(self) -> float:
+ """Return the maximum left window time to wait"""
+ max_time = 0
+ for response in self.variables().values():
+ # If we ever implement a response variable that does not have a left window, this will break
+ in_seconds = time_string_to_seconds(response.left_window)
+ if in_seconds > max_time:
+ max_time = in_seconds
+ return max_time
def get_metric_variables(self) -> list[MetricResponseVariable]:
"""Return the metric variables of this observer"""
diff --git a/oxn/responses.py b/oxn/responses.py
index ff68d65..e488a74 100644
--- a/oxn/responses.py
+++ b/oxn/responses.py
@@ -15,6 +15,9 @@
from .models.response import ResponseVariable
from .jaeger import Jaeger
from .prometheus import Prometheus
+import logging
+
+logger = logging.getLogger(__name__)
class MetricResponseVariable(ResponseVariable):
@@ -28,6 +31,8 @@ def __init__(
name: str,
experiment_start: float,
experiment_end: float,
+ right_window: str,
+ left_window: str,
description: dict,
target: str,
):
@@ -53,6 +58,8 @@ def __init__(
self.end = self.experiment_end + utils.time_string_to_seconds(
description["right_window"]
)
+ self.right_window = right_window
+ self.left_window = left_window
"""Timestamp of the end of the observation period relative to experiment end"""
self.prometheus = Prometheus(orchestrator=self.orchestrator, target=target)
"""Prometheus API to fetch metric data represented by this response variable"""
@@ -92,13 +99,12 @@ def label(
label_column: str,
label: str,
) -> None:
- """
- Label a Prometheus dataframe. Note that Prometheus returns timestamps in seconds as a float
-
- """
+ """Label a Prometheus dataframe. Note that Prometheus returns timestamps in seconds as a float"""
+ if self.data is None or self.data.empty:
+ self.data = pd.DataFrame(columns=['timestamp'])
+ return
predicate = self.data["timestamp"].between(treatment_start, treatment_end)
-
self.data[label_column] = np.where(predicate, label, "NoTreatment")
@staticmethod
@@ -166,20 +172,25 @@ def _range_query_to_df(self, json_data, metric_column_name):
)
def observe(self):
- prometheus_query = self.prometheus.build_query(
- metric_name=self.metric_name,
- label_dict=self.labels,
- )
- prometheus_metrics = self.prometheus.range_query(
- query=prometheus_query,
- start=self.start,
- end=self.end,
- step=self.step,
- )
- self.data = self._range_query_to_df(
- prometheus_metrics, metric_column_name=self.metric_name
- )
- return self.data
+ try:
+ prometheus_query = self.prometheus.build_query(
+ metric_name=self.metric_name,
+ label_dict=self.labels,
+ )
+ prometheus_metrics = self.prometheus.range_query(
+ query=prometheus_query,
+ start=self.start,
+ end=self.end,
+ step=self.step,
+ )
+ self.data = self._range_query_to_df(
+ prometheus_metrics, metric_column_name=self.metric_name
+ )
+ return self.data
+ except PrometheusException as e:
+ # Initialize with empty DataFrame instead of None
+ self.data = pd.DataFrame(columns=['timestamp'])
+ raise e
class TraceResponseVariable(ResponseVariable):
@@ -189,6 +200,8 @@ def __init__(
name: str,
experiment_start: float,
experiment_end: float,
+ right_window: str,
+ left_window: str,
description: dict,
):
super(TraceResponseVariable, self).__init__(
@@ -210,6 +223,8 @@ def __init__(
self.end = self.experiment_end + utils.time_string_to_seconds(
description["right_window"]
)
+ self.right_window = right_window
+ self.left_window = left_window
"""UTC Timestamp of the end of the observation period relative to the experiment end"""
self.jaeger = Jaeger(orchestrator=self.orchestrator)
"""Jaeger API to observe trace data"""
@@ -263,6 +278,11 @@ def label(
label: str,
) -> None:
"""Label a dataframe containing Jaeger spans depending on the span start timestamp"""
+ if self.data is None or self.data.empty:
+ # TODO handle this better
+ self.data = pd.DataFrame(columns=['start_time'])
+ return
+
scaled_treatment_start = utils.to_microseconds(treatment_start)
scaled_treatment_end = utils.to_microseconds(treatment_end)
predicate = (self.data["start_time"] >= scaled_treatment_start) & (
@@ -292,6 +312,11 @@ def _tabulate(trace_json) -> pd.DataFrame:
"end_time",
"duration",
"service_name",
+ "span_kind",
+ "req_status_code",
+ "ref_type",
+ "ref_type_span_ID",
+ "ref_type_trace_ID"
]
dataframes = []
@@ -309,7 +334,31 @@ def _tabulate(trace_json) -> pd.DataFrame:
process_id = span["processID"]
process_dict = processes.get(process_id)
service_name = process_dict["serviceName"]
- row = [trace_id, span_id, operation, start, end, duration, service_name]
+
+ # adding the span kind : internal / client / service / consumer / producer
+ # adding the status code of rRPC / http request or -1 if spankind == interna
+ req_status_code = "N/A"
+ span_kind = "N/A"
+ if "tags" in span and isinstance(span["tags"], list):
+ for obj in span["tags"]:
+ if obj["key"] == "span.kind":
+ span_kind = obj["value"]
+ if obj["key"] == "rpc.grpc.status_code":
+ req_status_code = obj["value"]
+ # here add http status code option
+ # since the application talks in gRPC and the Frontend and Frontend Proxy with with HTTP
+ if obj["key"] == "http.status_code":
+ req_status_code = obj["value"]
+ ref_type = "N/A"
+ ref_type_spanID = "N/A"
+ ref_type_traceID = "N/A"
+ if span["references"]:
+ ref_type = span["references"][0]["refType"]
+ ref_type_spanID = span["references"][0]["spanID"]
+ ref_type_traceID = span["references"][0]["traceID"]
+ # adding the reftype per span "CHILD_of" / "follows_from"
+ # adding the spankind span ID and the spankind trace
+ row = [trace_id, span_id, operation, start, end, duration, service_name, span_kind, req_status_code, ref_type, ref_type_spanID, ref_type_traceID]
trace_rows.append(row)
dataframe = pd.DataFrame(trace_rows, columns=columns)
dataframe["duration"] = pd.to_numeric(dataframe["duration"])
@@ -329,13 +378,17 @@ def _tabulate(trace_json) -> pd.DataFrame:
def observe(self) -> pd.DataFrame:
"""Observe the data service represented by this response variable"""
- traces = self.jaeger.search_traces(
- service_name=self.service_name,
- start=self._jaeger_start_timestamp,
- end=self._jaeger_end_timestamp,
- limit=self.limit,
- )
-
- trace_df = self._tabulate(trace_json=traces)
- self.data = trace_df
- return trace_df
+ try:
+ traces = self.jaeger.search_traces(
+ service_name=self.service_name,
+ start=self._jaeger_start_timestamp,
+ end=self._jaeger_end_timestamp,
+ limit=self.limit,
+ )
+ trace_df = self._tabulate(trace_json=traces)
+ self.data = trace_df
+ return trace_df
+ except JaegerException as e:
+ # TODO handle this better
+ self.data = pd.DataFrame(columns=['start_time'])
+ raise e
diff --git a/oxn/runner.py b/oxn/runner.py
index 4bd9711..f74be47 100644
--- a/oxn/runner.py
+++ b/oxn/runner.py
@@ -16,7 +16,7 @@
import docker
-from .errors import OxnException
+from .errors import OxnException, PrometheusException, JaegerException
from .treatments import (
DeploymentScaleTreatment,
EmptyDockerComposeTreatment,
@@ -259,9 +259,16 @@ def _label(self) -> None:
"""Label the observed data with information from the treatments"""
for treatment in self.treatments.values():
for response_id, response_variable in self.observer.variables().items():
- response_variable.label(
- treatment_end=treatment.end,
- treatment_start=treatment.start,
- label_column=treatment.name,
- label=treatment.name,
- )
+ try:
+ response_variable.label(
+ treatment_end=treatment.end,
+ treatment_start=treatment.start,
+ label_column=treatment.name,
+ label=treatment.name,
+ )
+ except (JaegerException, PrometheusException) as e:
+ logger.warning(f"Failed to label response variable {response_variable.name}: {str(e)}. Skipping.")
+ continue
+ except Exception as e:
+ logger.error(f"Unexpected error while labeling response variable {response_variable.name}: {str(e)}")
+ raise
diff --git a/oxn/schemas/experiment_schema.json b/oxn/schemas/experiment_schema.json
new file mode 100644
index 0000000..ec33296
--- /dev/null
+++ b/oxn/schemas/experiment_schema.json
@@ -0,0 +1,255 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "experiment": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "orchestrator": {
+ "type": "string"
+ },
+ "services": {
+ "type": "object",
+ "properties": {
+ "jaeger": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "namespace"
+ ]
+ },
+ "prometheus": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "namespace",
+ "target"
+ ]
+ }
+ }
+ },
+ "required": [
+ "jaeger",
+ "prometheus"
+ ]
+ },
+ "responses": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ },
+ "metric_name": {
+ "type": "string"
+ },
+ "type": {
+ "const": "metric"
+ },
+ "step": {
+ "type": "integer"
+ },
+ "left_window": {
+ "type": "string"
+ },
+ "right_window": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "target",
+ "metric_name",
+ "type",
+ "step",
+ "left_window",
+ "right_window"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "type": {
+ "const": "trace"
+ },
+ "service_name": {
+ "type": "string"
+ },
+ "left_window": {
+ "type": "string"
+ },
+ "right_window": {
+ "type": "string"
+ },
+ "limit": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "name",
+ "type",
+ "service_name",
+ "left_window",
+ "right_window"
+ ]
+ }
+ ]
+ }
+ },
+ "treatments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "params": {
+ "type": "object"
+ }
+ },
+ "required": [
+ "action",
+ "params"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "sue": {
+ "type": "object",
+ "properties": {
+ "compose": {
+ "type": "string"
+ },
+ "exclude": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "include": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "required": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "namespace": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "namespace",
+ "name"
+ ]
+ }
+ }
+ },
+ "required": [
+ "compose"
+ ]
+ },
+ "loadgen": {
+ "type": "object",
+ "properties": {
+ "run_time": {
+ "type": "string"
+ },
+ "max_users": {
+ "type": "integer"
+ },
+ "spawn_rate": {
+ "type": "integer"
+ },
+ "locust_files": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "target": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "name",
+ "namespace",
+ "port"
+ ]
+ }
+ },
+ "required": [
+ "run_time"
+ ]
+ }
+ },
+ "required": [
+ "version",
+ "orchestrator",
+ "responses",
+ "sue",
+ "loadgen"
+ ]
+ }
+ },
+ "required": [
+ "experiment"
+ ]
+}
\ No newline at end of file
diff --git a/oxn/settings.py b/oxn/settings.py
index c1fb7de..a6228e1 100644
--- a/oxn/settings.py
+++ b/oxn/settings.py
@@ -4,5 +4,10 @@
Connection: Referenced throughout the application for consistent configuration values.
"""
+import os
+
+
STORAGE_NAME = "store.h5"
TRIE_NAME = "trie.pickle"
+SCHEMA_PATH = os.path.join(os.path.dirname(__file__), 'schemas', 'experiment_schema.json')
+STORAGE_DIR = os.path.join(os.path.dirname(__file__), 'data')
\ No newline at end of file
diff --git a/oxn/store.py b/oxn/store.py
index e5dd7fd..f028323 100644
--- a/oxn/store.py
+++ b/oxn/store.py
@@ -4,22 +4,36 @@
Connection: Used by various components to persist and retrieve experimental data.
-Simple HDF5-based storage
+Simple HDF5-based storage and JSON export
"""
import pickle
import warnings
-
+import json
+import os
+from pathlib import Path
import pandas as pd
-from typing import List
-from .settings import STORAGE_NAME, TRIE_NAME
+from typing import List, Dict
+from .settings import STORAGE_NAME, TRIE_NAME, STORAGE_DIR
# silence warning that we cant use hex strings as key names
# we don't want table accessing by dot notation
# due to fixed format in hdf
from tables import NaturalNameWarning
-
warnings.filterwarnings("ignore", category=NaturalNameWarning)
+# Global variable to store the configured output path
+_configured_path = None
+
+def configure_output_path(path: str) -> None:
+ """Configure the output path for HDF storage"""
+ global _configured_path
+ _configured_path = path
+
+def _get_storage_path() -> Path:
+ """Get the full storage path based on configured path or default"""
+ storage_dir = Path(_configured_path if _configured_path else STORAGE_DIR)
+ storage_dir.mkdir(parents=True, exist_ok=True)
+ return storage_dir / STORAGE_NAME
class Node:
"""A node in the trie structure"""
@@ -112,7 +126,8 @@ def construct_key(experiment_key, run_key, response_key):
def write_dataframe(dataframe, experiment_key, run_key, response_key) -> None:
"""Write a dataframe to the store"""
- with pd.HDFStore(STORAGE_NAME) as store:
+ store_path = _get_storage_path()
+ with pd.HDFStore(store_path) as store:
key = construct_key(experiment_key, run_key, response_key)
store.put(key=key, value=dataframe)
trie = Trie()
@@ -124,19 +139,19 @@ def get_dataframe(key):
trie = Trie()
results = trie.query(key)
if results:
- with pd.HDFStore(STORAGE_NAME) as store:
+ with pd.HDFStore(_get_storage_path()) as store:
return store.get(key=key)
def annotate(key, **kwargs):
"""Annotate a stored response variable with metadata"""
- with pd.HDFStore(STORAGE_NAME) as store:
+ with pd.HDFStore(_get_storage_path()) as store:
store.get_storer(key).attrs.metadata = kwargs
def remove_dataframe(key) -> None:
"""Remove a dataframe from the store"""
- with pd.HDFStore(STORAGE_NAME) as store:
+ with pd.HDFStore(_get_storage_path()) as store:
store.remove(key=key)
@@ -170,5 +185,19 @@ def list_keys_for_run(experiment_key, experiment_run) -> List[str]:
def list_all_dataframes():
"""List all dataframes in the store"""
- with pd.HDFStore(STORAGE_NAME) as store:
+ with pd.HDFStore(_get_storage_path()) as store:
return store.keys(include="pandas")
+
+def write_json_data(data, experiment_key, run_key, response_key, out_path=None) -> None:
+ """Write data to a JSON file"""
+ storage_dir = out_path if out_path else STORAGE_DIR
+ Path(storage_dir).mkdir(parents=True, exist_ok=True)
+
+ if isinstance(data, pd.DataFrame):
+ data = data.to_dict(orient='records')
+
+ filename = f"{experiment_key}_{run_key}_{response_key}.json"
+ json_path = Path(storage_dir) / filename
+
+ with open(json_path, 'x') as f:
+ json.dump(data, f, indent=2)
\ No newline at end of file
diff --git a/oxn/tests/unit/test_schema_validation.py b/oxn/tests/unit/test_schema_validation.py
new file mode 100644
index 0000000..3f2d469
--- /dev/null
+++ b/oxn/tests/unit/test_schema_validation.py
@@ -0,0 +1,136 @@
+import unittest
+import json
+import os
+import yaml
+from jsonschema import validate
+
+from oxn.validation import load_schema
+from oxn.errors import OxnException
+
+class SchemaValidationTest(unittest.TestCase):
+ def setUp(self):
+ self.schema = load_schema()
+ self.valid_spec = {
+ "experiment": {
+ "name": "k8s-test-successful",
+ "version": "0.0.1",
+ "orchestrator": "kubernetes",
+ "services": {
+ "jaeger": {
+ "name": "astronomy-shop-jaeger-query",
+ "namespace": "system-under-evaluation"
+ },
+ "prometheus": [
+ {
+ "name": "astronomy-shop-prometheus-server",
+ "namespace": "system-under-evaluation",
+ "target": "sue"
+ },
+ {
+ "name": "kube-prometheus-kube-prome-prometheus",
+ "namespace": "oxn-external-monitoring",
+ "target": "oxn"
+ }
+ ]
+ },
+ "responses": [
+ {
+ "name": "frontend_traces",
+ "type": "trace",
+ "service_name": "frontend",
+ "left_window": "60s",
+ "right_window": "60s",
+ "target": "sue",
+ "limit": 10000
+ },
+ {
+ "name": "shippingservice_traces",
+ "type": "trace",
+ "service_name": "shippingservice",
+ "left_window": "60s",
+ "right_window": "60s",
+ "limit": 10000
+ },
+ {
+ "name": "system_CPU",
+ "type": "metric",
+ "metric_name": "sum(rate(container_cpu_usage_seconds_total{namespace=\"system-under-evaluation\"}[1m]))",
+ "left_window": "60s",
+ "right_window": "60s",
+ "step": 1,
+ "target": "oxn"
+ },
+ {
+ "name": "latency_recommendationservice_95_percentile",
+ "type": "metric",
+ "metric_name": "histogram_quantile(0.95, sum(rate(duration_milliseconds_bucket{service_name=\"recommendationservice\"}[90s])) by (le))",
+ "left_window": "300s",
+ "right_window": "300s",
+ "step": 1,
+ "target": "sue"
+ }
+ ],
+ "treatments": [
+ {
+ "empty_treatment": {
+ "action": "empty",
+ "params": {"duration": "1m"}
+ }
+ }
+ ],
+ "sue": {
+ "compose": "opentelemetry-demo/docker-compose.yml",
+ "exclude": ["loadgenerator"],
+ "required": [
+ {
+ "namespace": "system-under-evaluation",
+ "name": "astronomy-shop-prometheus-server"
+ }
+ ]
+ },
+ "loadgen": {
+ "run_time": "2m",
+ "max_users": 10,
+ "spawn_rate": 5,
+ "locust_files": [
+ "/opt/oxn/locust/locust_basic_interaction.py",
+ "/opt/oxn/locust/locust_otel_demo.py"
+ ],
+ "target": {
+ "name": "astronomy-shop-frontendproxy",
+ "namespace": "system-under-evaluation",
+ "port": 8080
+ }
+ }
+ }
+ }
+
+ def test_valid_experiment_spec(self):
+ """Test that a valid experiment spec passes validation"""
+ # Should not raise an exception
+ validate(instance=self.valid_spec, schema=self.schema)
+
+ def test_invalid_experiment_spec(self):
+ """Test that an invalid experiment spec fails validation"""
+ invalid_spec = {
+ "experiment": {
+ # Missing required fields
+ "responses": []
+ }
+ }
+
+ with self.assertRaises(Exception):
+ validate(instance=invalid_spec, schema=self.schema)
+
+ def test_real_experiment_files(self):
+ """Test that our actual experiment files pass validation"""
+ experiments_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'experiments')
+
+ for filename in os.listdir(experiments_dir):
+ if filename.endswith('.yml') or filename.endswith('.yaml'):
+ with open(os.path.join(experiments_dir, filename)) as f:
+ spec = yaml.safe_load(f)
+ try:
+ validate(instance=spec, schema=self.schema)
+ except Exception as e:
+ self.fail(f"Validation failed for {filename}: {str(e)}")
\ No newline at end of file
diff --git a/oxn/validation.py b/oxn/validation.py
index 3fede50..a3ab732 100644
--- a/oxn/validation.py
+++ b/oxn/validation.py
@@ -2,51 +2,55 @@
Purpose: Validates experiment specifications.
Functionality: Implements syntactic and semantic validation of experiment configurations.
Connection: Used by the Engine to ensure the experiment configuration is correct before execution.
-
-Module to handle validation of experiment specifications"""
+"""
+import os
+import json
+import jsonschema
from typing import Set, List
-
-import schema
-
-from .models import orchestrator
+from pathlib import Path
from .errors import OxnException
from .jaeger import Jaeger
from .prometheus import Prometheus
+from .settings import SCHEMA_PATH
+def load_schema():
+ """Load the JSON schema file"""
+ with open(SCHEMA_PATH) as f:
+ return json.load(f)
class SemanticValidator:
"""Semantic validation for experiment specifications"""
def __init__(self, experiment_spec: dict):
self.experiment_spec = experiment_spec
- """The experiment specification to validate"""
self.prometheus = Prometheus()
- """API to Prometheus required for label values and metric names"""
self.jaeger = Jaeger()
- """API to Jaeger to required for service names"""
self.metric_names = None
- """A set of metric names from Prometheus"""
self.label_names = None
- """A set of label names from Prometheus"""
self.label_values = None
- """A set of label values from Prometheus"""
self.service_names = None
- """A set of service names from Jaeger"""
- self.metric_rvar_type = "metric"
- """Constant to represent the metric rvar type"""
- self.trace_rvar_type = "trace"
- """Constant to represent the trace rvar type"""
self.messages: List[str] = []
- """List of messages to pass to CLI in case of failing validation"""
+
+ # Perform syntactic validation
+ self.validate_syntax()
+
+ # Then populate data for semantic validation
self._populate_metrics()
- """Populate the metric name set"""
self._populate_labels()
- """Populate the label name set"""
self._populate_label_values()
- """Populate the label value set"""
self._populate_service_names()
- """Populate the service name set """
+
+ def validate_syntax(self):
+ """Validate the experiment specification against JSON schema"""
+ try:
+ schema = load_schema()
+ jsonschema.validate(instance=self.experiment_spec, schema=schema)
+ except jsonschema.exceptions.ValidationError as e:
+ raise OxnException(
+ message="Experiment specification failed JSON schema validation",
+ explanation=str(e)
+ )
def _populate_service_names(self) -> Set[str]:
"""Call jaeger to get a list of service names from Jaeger traces"""
@@ -158,86 +162,3 @@ def validate(self):
message="Experiment specification did not pass semantic validation",
explanation=message,
)
-
-
-metric_response_schema = {
- str: {
- "target": str,
- "metric_name": str,
- "type": "metric",
- "step": int,
- "left_window": str,
- "right_window": str,
- schema.Optional("labels"): {schema.Optional(str): schema.Optional(str)},
- }
-}
-"""Schema to validate metric responses syntactically"""
-
-trace_response_schema = {
- str: {
- "type": "trace",
- "service_name": str,
- "left_window": str,
- "right_window": str,
- schema.Optional("limit"): int,
- }
-}
-"""Schema to validate trace responses syntactically"""
-
-syntactic_schema = schema.Schema(
- {
- "experiment": {
- "version": str,
- "orchestrator": str,
- schema.Optional("services"): {
- "jaeger": {
- "name": str,
- "namespace": str,
- },
- # prometheus is a list containing multiple dicts with the paramters name, namespace and target
- "prometheus": [
- {
- "name": str,
- "namespace": str,
- "target": str,
- }
- ],
-
- },
- "responses": [
- schema.Or(metric_response_schema, trace_response_schema),
- ],
- schema.Optional("treatments"): [
- {
- str: {
- "action": str,
- "params": dict,
- }
- }
- ],
- "sue": {
- "compose": str,
- schema.Optional("exclude"): list[str],
- schema.Optional("include"): list[str],
- schema.Optional("required"): [
- {
- "namespace": str,
- "name": str,
- }
- ]
- },
- "loadgen": {
- "run_time": str,
- schema.Optional("max_users"): int,
- schema.Optional("spawn_rate"): int,
- schema.Optional("locust_files"): list[str],
- schema.Optional("target"): {
- "name": str,
- "namespace": str,
- "port": int,
- },
- },
- },
- }
-)
-"""Schema to validate an experiment specification syntactically"""
diff --git a/setup.cfg b/setup.cfg
index 0d2a387..0091d4f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -35,6 +35,10 @@ install_requires =
python-on-whales>=0.59.0
scipy>=1.10.1
tables>=3.8.0
+ django>=5.1
+ djangorestframework>=3.12
+ kubernetes>=28.1.0
+ jsonschema>=4.19.0
[options.packages.find]
include = oxn*
diff --git a/setup.py b/setup.py
index 8bf1ba9..725e65e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,2 +1,7 @@
from setuptools import setup
-setup()
+setup(
+ package_data={
+ 'oxn': ['schemas/*.json'],
+ },
+ include_package_data=True,
+)
\ No newline at end of file
diff --git a/values_opentelemetry_demo.yaml b/values_opentelemetry_demo.yaml
deleted file mode 100644
index fca023b..0000000
--- a/values_opentelemetry_demo.yaml
+++ /dev/null
@@ -1,134 +0,0 @@
-jaeger:
- allInOne:
- resources:
- requests:
- memory: 5Gi
- limits:
- memory: 5Gi
- # args:
- # - --query.base-path=/jaeger/ui
- # - --prometheus.server-url=http://{{ include "otel-demo.name" . }}-prometheus-server:9090
- # - --prometheus.query.normalize-calls=true
- # - --prometheus.query.normalize-duration=true
- # storage:
- # badger:
- # ephemeral: false
- # persistence:
- # mountPath: /mnt/data
- # useExistingPvcName: "jaeger-pvc"
- # type: badger
-
-components:
- flagd:
- resources:
- limits:
- memory: 150Mi
- productCatalogService:
- resources:
- limits:
- memory: 150Mi
- loadgenerator:
- enabled: false
- checkoutService:
- resources:
- limits:
- memory: 150Mi
-
-opentelemetry-collector:
- config:
- processors:
- probabilistic_sampler:
- sampling_percentage: 1
- hash_seed: 22
- service:
- pipelines:
- traces:
- processors:
- - memory_limiter
- - resource
- - transform
- - batch
- - probabilistic_sampler
-prometheus:
- server:
- extraArgs:
- web.enable-lifecycle: ""
- persistentVolume:
- enabled: true
- storageClass: "openebs-hostpath"
- accessModes:
- - ReadWriteOnce
- size: 8Gi
- resources:
- limits:
- memory: 1500Mi
-
- #prometheus-node-exporter:
- # enabled: true
- # podAnnotations:
- # prometheus.io/scrape: "true"
- # prometheus.io/port: "9100"
- # opentelemetry_community_demo: "true"
- #kube-state-metrics:
- # enabled: true
- # podAnnotations:
- # prometheus.io/scrape: "true"
- # prometheus.io/port: "8080"
- # opentelemetry_community_demo: "true"
- #server:
- # global:
- # scrape_interval: 5s
- #serverFiles:
- # prometheus.yml:
- # scrape_configs:
- # - job_name: 'kubernetes-pods'
- # kubernetes_sd_configs:
- # - role: pod
- # relabel_configs:
- # - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
- # action: keep
- # regex: true
- # - job_name: 'otel-collector'
- # honor_labels: true
- # kubernetes_sd_configs:
- # - role: pod
- # namespaces:
- # own_namespace: true
- # relabel_configs:
- # - source_labels: [__meta_kubernetes_pod_annotation_opentelemetry_community_demo]
- # action: keep
- # regex: true
- # - job_name: 'kubernetes-service-endpoints'
- # scrape_interval: 5s
- # scrape_timeout: 2s
- # kubernetes_sd_configs:
- # - role: service
- # relabel_configs:
- # - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
- # action: keep
- # regex: true
- # - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
- # action: replace
- # target_label: __scheme__
- # regex: (https?)
- # - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
- # action: replace
- # target_label: __metrics_path__
- # regex: (.+)
- # - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
- # action: replace
- # target_label: __address__
- # regex: (.+)(?::\d+);(\d+)
- # replacement: $1:$2
- # - action: labelmap
- # regex: __meta_kubernetes_service_label_(.+)
- # - source_labels: [__meta_kubernetes_namespace]
- # action: replace
- # target_label: kubernetes_namespace
- # - source_labels: [__meta_kubernetes_service_name]
- # action: replace
- # target_label: kubernetes_name
- # - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
- # action: replace
- # target_label: __scheme__
- # regex: (.+)
\ No newline at end of file