diff --git a/.build/dev/nginx.conf b/.build/dev/nginx.conf
index 4a47885..e68601a 100644
--- a/.build/dev/nginx.conf
+++ b/.build/dev/nginx.conf
@@ -7,6 +7,11 @@ server {
location /api/ {
proxy_pass http://backend:8000/api/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws/ {
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e8f2c70
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,18 @@
+# Database Configuration
+DATABASE_URL=postgresql://mapmaker:mapmaker@database:5432/mapmaker
+
+# Security
+SECRET_KEY=generate-secure-random-key-change-in-production
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+REFRESH_TOKEN_EXPIRE_DAYS=7
+
+# Registration
+# Set to "true" to allow new user registration, "false" to disable
+ALLOW_REGISTRATION=false
+
+# CORS (comma-separated origins)
+ALLOWED_ORIGINS=http://localhost:8000,http://localhost:3000
+
+# Environment
+ENVIRONMENT=development
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..69c067e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,78 @@
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# Virtual Environment
+venv/
+ENV/
+env/
+.venv
+
+# PyCharm
+.idea/
+
+# VS Code
+.vscode/
+
+# Pytest
+.pytest_cache/
+.coverage
+htmlcov/
+
+# Alembic
+# Don't ignore migrations, but ignore these:
+*.pyc
+
+# Node.js / Frontend
+node_modules/
+public/node_modules/
+public/dist/
+public/build/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# Frontend build
+public/.vite/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+
+# Logs
+logs/
+*.log
+
+# Claude Code
+.claude/plans/
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..9d5afee
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,87 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = alembic
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python-dateutil library that can be
+# installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to alembic/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/alembic/env.py b/alembic/env.py
new file mode 100644
index 0000000..89ce025
--- /dev/null
+++ b/alembic/env.py
@@ -0,0 +1,79 @@
+from logging.config import fileConfig
+from sqlalchemy import engine_from_config, pool
+from alembic import context
+import sys
+from pathlib import Path
+
+# Add the parent directory to the path to import app modules
+sys.path.append(str(Path(__file__).resolve().parents[1]))
+
+from app.database import Base
+from app.config import settings
+from app.models import * # Import all models
+
+# this is the Alembic Config object
+config = context.config
+
+# Interpret the config file for Python logging.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# Set the SQLAlchemy URL from settings
+config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
+
+# add your model's MetaData object here for 'autogenerate' support
+target_metadata = Base.metadata
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata,
+ compare_type=True # Enable type comparison for migrations
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/alembic/script.py.mako b/alembic/script.py.mako
new file mode 100644
index 0000000..69f9722
--- /dev/null
+++ b/alembic/script.py.mako
@@ -0,0 +1,25 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import geoalchemy2
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/alembic/versions/20251212_0125_915e5889d6d7_initial_schema_with_postgis_support.py b/alembic/versions/20251212_0125_915e5889d6d7_initial_schema_with_postgis_support.py
new file mode 100644
index 0000000..dcd28e5
--- /dev/null
+++ b/alembic/versions/20251212_0125_915e5889d6d7_initial_schema_with_postgis_support.py
@@ -0,0 +1,122 @@
+"""Initial schema with PostGIS support
+
+Revision ID: 915e5889d6d7
+Revises:
+Create Date: 2025-12-12 01:25:16.706772
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import geoalchemy2
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '915e5889d6d7'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # Enable PostGIS extension
+ op.execute('CREATE EXTENSION IF NOT EXISTS postgis')
+
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('users',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('username', sa.String(length=50), nullable=False),
+ sa.Column('email', sa.String(length=255), nullable=False),
+ sa.Column('password_hash', sa.String(length=255), nullable=False),
+ sa.Column('is_admin', sa.Boolean(), nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
+ op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
+ op.create_table('maps',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('name', sa.String(length=255), nullable=False),
+ sa.Column('description', sa.Text(), nullable=True),
+ sa.Column('owner_id', sa.UUID(), nullable=False),
+ sa.Column('is_default_public', sa.Boolean(), nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_maps_owner_id'), 'maps', ['owner_id'], unique=False)
+ op.create_table('map_items',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('map_id', sa.UUID(), nullable=False),
+ sa.Column('type', sa.String(length=50), nullable=False),
+ sa.Column('geometry', geoalchemy2.types.Geography(srid=4326, from_text='ST_GeogFromText', name='geography', nullable=False), nullable=False),
+ sa.Column('properties', postgresql.JSONB(astext_type=sa.Text()), server_default='{}', nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('created_by', sa.UUID(), nullable=True),
+ sa.Column('updated_by', sa.UUID(), nullable=True),
+ sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
+ sa.ForeignKeyConstraint(['map_id'], ['maps.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # Note: GeoAlchemy2 automatically creates a GIST index on Geography columns
+ # So we don't need to create it explicitly here
+ # op.create_index('idx_map_items_geometry', 'map_items', ['geometry'], unique=False, postgresql_using='gist')
+ op.create_index(op.f('ix_map_items_map_id'), 'map_items', ['map_id'], unique=False)
+ op.create_index(op.f('ix_map_items_type'), 'map_items', ['type'], unique=False)
+ op.create_table('sessions',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('user_id', sa.UUID(), nullable=True),
+ sa.Column('map_id', sa.UUID(), nullable=False),
+ sa.Column('socket_id', sa.String(length=255), nullable=False),
+ sa.Column('username', sa.String(length=50), nullable=True),
+ sa.Column('connected_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('last_seen', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.ForeignKeyConstraint(['map_id'], ['maps.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_sessions_map_id'), 'sessions', ['map_id'], unique=False)
+ op.create_index(op.f('ix_sessions_user_id'), 'sessions', ['user_id'], unique=False)
+ op.create_table('shares',
+ sa.Column('id', sa.UUID(), nullable=False),
+ sa.Column('map_id', sa.UUID(), nullable=False),
+ sa.Column('share_token', sa.String(length=64), nullable=False),
+ sa.Column('access_level', sa.String(length=20), nullable=False),
+ sa.Column('requires_auth', sa.Boolean(), nullable=False),
+ sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('created_by', sa.UUID(), nullable=True),
+ sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
+ sa.ForeignKeyConstraint(['map_id'], ['maps.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_shares_map_id'), 'shares', ['map_id'], unique=False)
+ op.create_index(op.f('ix_shares_share_token'), 'shares', ['share_token'], unique=True)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ # Drop our application tables
+ op.drop_index(op.f('ix_shares_share_token'), table_name='shares')
+ op.drop_index(op.f('ix_shares_map_id'), table_name='shares')
+ op.drop_table('shares')
+ op.drop_index(op.f('ix_sessions_user_id'), table_name='sessions')
+ op.drop_index(op.f('ix_sessions_map_id'), table_name='sessions')
+ op.drop_table('sessions')
+ op.drop_index(op.f('ix_map_items_type'), table_name='map_items')
+ op.drop_index(op.f('ix_map_items_map_id'), table_name='map_items')
+ op.drop_index('idx_map_items_geometry', table_name='map_items', postgresql_using='gist')
+ op.drop_table('map_items')
+ op.drop_index(op.f('ix_maps_owner_id'), table_name='maps')
+ op.drop_table('maps')
+ op.drop_index(op.f('ix_users_username'), table_name='users')
+ op.drop_index(op.f('ix_users_email'), table_name='users')
+ op.drop_table('users')
+
+ # Optionally drop PostGIS extension (commented out to preserve it)
+ # op.execute('DROP EXTENSION IF EXISTS postgis CASCADE')
+ # ### end Alembic commands ###
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/config.py b/app/config.py
new file mode 100644
index 0000000..9f7ddad
--- /dev/null
+++ b/app/config.py
@@ -0,0 +1,39 @@
+from pydantic_settings import BaseSettings, SettingsConfigDict
+from typing import List
+
+
+class Settings(BaseSettings):
+ """Application settings loaded from environment variables."""
+
+ # Database
+ DATABASE_URL: str
+
+ # Security
+ SECRET_KEY: str
+ ALGORITHM: str = "HS256"
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
+ REFRESH_TOKEN_EXPIRE_DAYS: int = 7
+
+ # Registration
+ ALLOW_REGISTRATION: bool = False
+
+ # CORS
+ ALLOWED_ORIGINS: str = "http://localhost:8000"
+
+ # Environment
+ ENVIRONMENT: str = "development"
+
+ model_config = SettingsConfigDict(
+ env_file=".env",
+ env_file_encoding="utf-8",
+ case_sensitive=True
+ )
+
+ @property
+ def cors_origins(self) -> List[str]:
+ """Parse comma-separated CORS origins into a list."""
+ return [origin.strip() for origin in self.ALLOWED_ORIGINS.split(",")]
+
+
+# Global settings instance
+settings = Settings()
diff --git a/app/database.py b/app/database.py
new file mode 100644
index 0000000..c9f7943
--- /dev/null
+++ b/app/database.py
@@ -0,0 +1,29 @@
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from app.config import settings
+
+# Create SQLAlchemy engine
+engine = create_engine(
+ settings.DATABASE_URL,
+ pool_pre_ping=True,
+ echo=settings.ENVIRONMENT == "development"
+)
+
+# Create SessionLocal class
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+# Base class for models
+Base = declarative_base()
+
+
+def get_db():
+ """
+ Dependency function to get database session.
+ Yields a database session and ensures it's closed after use.
+ """
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
diff --git a/app/dependencies.py b/app/dependencies.py
new file mode 100644
index 0000000..aeb922e
--- /dev/null
+++ b/app/dependencies.py
@@ -0,0 +1,94 @@
+from typing import Optional
+from fastapi import Depends, HTTPException, status
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from sqlalchemy.orm import Session
+from jose import JWTError, jwt
+from app.database import get_db
+from app.config import settings
+from app.models.user import User
+
+# Security scheme for JWT
+security = HTTPBearer()
+
+
+async def get_current_user(
+ credentials: HTTPAuthorizationCredentials = Depends(security),
+ db: Session = Depends(get_db)
+) -> User:
+ """
+ Dependency to get the current authenticated user from JWT token.
+ Raises HTTP 401 if token is invalid or user not found.
+ """
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ try:
+ # Decode JWT token
+ token = credentials.credentials
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
+ user_id: str = payload.get("sub")
+ if user_id is None:
+ raise credentials_exception
+ except JWTError:
+ raise credentials_exception
+
+ # Get user from database
+ user = db.query(User).filter(User.id == user_id).first()
+ if user is None:
+ raise credentials_exception
+
+ return user
+
+
+async def get_current_active_user(
+ current_user: User = Depends(get_current_user)
+) -> User:
+ """
+ Dependency to get the current active user.
+ Can be extended to check if user is active/banned.
+ """
+ # Add user.is_active check here if needed in the future
+ return current_user
+
+
+async def get_current_admin_user(
+ current_user: User = Depends(get_current_user)
+) -> User:
+ """
+ Dependency to get the current admin user.
+ Raises HTTP 403 if user is not an admin.
+ """
+ if not current_user.is_admin:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not enough permissions"
+ )
+ return current_user
+
+
+async def get_optional_current_user(
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
+ db: Session = Depends(get_db)
+) -> Optional[User]:
+ """
+ Dependency to optionally get the current user.
+ Returns None if no token provided or token is invalid.
+ Useful for endpoints that work for both authenticated and guest users.
+ """
+ if credentials is None:
+ return None
+
+ try:
+ token = credentials.credentials
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
+ user_id: str = payload.get("sub")
+ if user_id is None:
+ return None
+
+ user = db.query(User).filter(User.id == user_id).first()
+ return user
+ except JWTError:
+ return None
diff --git a/app/main.py b/app/main.py
new file mode 100644
index 0000000..cad0eda
--- /dev/null
+++ b/app/main.py
@@ -0,0 +1,44 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from app.config import settings
+from app.routers import auth, maps, items
+
+# Create FastAPI application
+app = FastAPI(
+ title="ISP Wiremap API",
+ description="API for ISP cable and network infrastructure mapping",
+ version="1.0.0"
+)
+
+# Configure CORS
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Register routers
+app.include_router(auth.router)
+app.include_router(maps.router)
+app.include_router(items.router)
+
+
+@app.get("/")
+async def root():
+ """Root endpoint for API health check."""
+ return {
+ "message": "ISP Wiremap API",
+ "version": "1.0.0",
+ "status": "running"
+ }
+
+
+@app.get("/api/health")
+async def health_check():
+ """Health check endpoint for monitoring."""
+ return {
+ "status": "healthy",
+ "environment": settings.ENVIRONMENT
+ }
diff --git a/app/models/__init__.py b/app/models/__init__.py
new file mode 100644
index 0000000..24b4758
--- /dev/null
+++ b/app/models/__init__.py
@@ -0,0 +1,7 @@
+from app.models.user import User
+from app.models.map import Map
+from app.models.map_item import MapItem
+from app.models.share import Share
+from app.models.session import Session
+
+__all__ = ["User", "Map", "MapItem", "Share", "Session"]
diff --git a/app/models/map.py b/app/models/map.py
new file mode 100644
index 0000000..fde1486
--- /dev/null
+++ b/app/models/map.py
@@ -0,0 +1,19 @@
+from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.sql import func
+import uuid
+from app.database import Base
+
+
+class Map(Base):
+ """Map model for storing map metadata and ownership."""
+
+ __tablename__ = "maps"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ name = Column(String(255), nullable=False)
+ description = Column(Text, nullable=True)
+ owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
+ is_default_public = Column(Boolean, default=False, nullable=False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+ updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
diff --git a/app/models/map_item.py b/app/models/map_item.py
new file mode 100644
index 0000000..e6e2ae6
--- /dev/null
+++ b/app/models/map_item.py
@@ -0,0 +1,46 @@
+from sqlalchemy import Column, String, DateTime, ForeignKey
+from sqlalchemy.dialects.postgresql import UUID, JSONB
+from sqlalchemy.sql import func
+from geoalchemy2 import Geography
+import uuid
+from app.database import Base
+
+
+class MapItem(Base):
+ """
+ Map item model for storing cables, switches, APs, and wireless mesh.
+
+ Types:
+ - cable: Fiber, Cat6, Cat6 PoE
+ - wireless_mesh: Wireless connections between APs
+ - switch: Network switch
+ - indoor_ap: Indoor access point
+ - outdoor_ap: Outdoor access point
+
+ Geometry:
+ - Point for devices (switches, APs)
+ - LineString for cables and wireless mesh
+
+ Properties (JSONB):
+ - For cables: cable_type, name, notes, length_meters, start_device_id, end_device_id
+ - For devices: name, notes, port_count, connections (array of {cable_id, port_number})
+ - For wireless_mesh: name, notes, start_ap_id, end_ap_id
+ """
+
+ __tablename__ = "map_items"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ map_id = Column(UUID(as_uuid=True), ForeignKey("maps.id", ondelete="CASCADE"), nullable=False, index=True)
+ type = Column(String(50), nullable=False, index=True)
+
+ # PostGIS geography column for spatial data
+ # Using GeometryCollection to support both Point and LineString geometries
+ geometry = Column(Geography(geometry_type='GEOMETRY', srid=4326), nullable=False)
+
+ # JSONB column for flexible properties based on item type
+ properties = Column(JSONB, nullable=False, server_default='{}')
+
+ created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+ updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
+ created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
+ updated_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
diff --git a/app/models/session.py b/app/models/session.py
new file mode 100644
index 0000000..678c63b
--- /dev/null
+++ b/app/models/session.py
@@ -0,0 +1,22 @@
+from sqlalchemy import Column, String, DateTime, ForeignKey
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.sql import func
+import uuid
+from app.database import Base
+
+
+class Session(Base):
+ """Session model for tracking WebSocket connections and user presence."""
+
+ __tablename__ = "sessions"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True)
+ map_id = Column(UUID(as_uuid=True), ForeignKey("maps.id", ondelete="CASCADE"), nullable=False, index=True)
+ socket_id = Column(String(255), nullable=False)
+
+ # Denormalized username for quick access
+ username = Column(String(50), nullable=True)
+
+ connected_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+ last_seen = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
diff --git a/app/models/share.py b/app/models/share.py
new file mode 100644
index 0000000..00d3608
--- /dev/null
+++ b/app/models/share.py
@@ -0,0 +1,27 @@
+from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.sql import func
+import uuid
+from app.database import Base
+
+
+class Share(Base):
+ """Share model for managing map access via share links."""
+
+ __tablename__ = "shares"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ map_id = Column(UUID(as_uuid=True), ForeignKey("maps.id", ondelete="CASCADE"), nullable=False, index=True)
+ share_token = Column(String(64), unique=True, nullable=False, index=True)
+
+ # Access levels: 'read_only', 'edit', 'guest_read_only'
+ access_level = Column(String(20), nullable=False)
+
+ # Whether authentication is required to use this share link
+ requires_auth = Column(Boolean, default=True, nullable=False)
+
+ # Optional expiration date
+ expires_at = Column(DateTime(timezone=True), nullable=True)
+
+ created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+ created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
diff --git a/app/models/user.py b/app/models/user.py
new file mode 100644
index 0000000..8437ff7
--- /dev/null
+++ b/app/models/user.py
@@ -0,0 +1,19 @@
+from sqlalchemy import Column, String, Boolean, DateTime
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.sql import func
+import uuid
+from app.database import Base
+
+
+class User(Base):
+ """User model for authentication and ownership."""
+
+ __tablename__ = "users"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ username = Column(String(50), unique=True, nullable=False, index=True)
+ email = Column(String(255), unique=True, nullable=False, index=True)
+ password_hash = Column(String(255), nullable=False)
+ is_admin = Column(Boolean, default=False, nullable=False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+ updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
diff --git a/app/routers/__init__.py b/app/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/routers/auth.py b/app/routers/auth.py
new file mode 100644
index 0000000..999d992
--- /dev/null
+++ b/app/routers/auth.py
@@ -0,0 +1,80 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from app.database import get_db
+from app.dependencies import get_current_user
+from app.schemas.auth import LoginRequest, TokenResponse
+from app.schemas.user import UserCreate, UserResponse, UserWithToken
+from app.services.auth_service import authenticate_user, create_user, create_tokens_for_user
+from app.models.user import User
+
+router = APIRouter(prefix="/api/auth", tags=["authentication"])
+
+
+@router.post("/register", response_model=UserWithToken, status_code=status.HTTP_201_CREATED)
+async def register(
+ user_data: UserCreate,
+ db: Session = Depends(get_db)
+):
+ """
+ Register a new user.
+ Registration must be enabled via ALLOW_REGISTRATION environment variable.
+ """
+ user = create_user(db, user_data)
+ tokens = create_tokens_for_user(user)
+
+ return UserWithToken(
+ id=user.id,
+ username=user.username,
+ email=user.email,
+ is_admin=user.is_admin,
+ created_at=user.created_at,
+ updated_at=user.updated_at,
+ access_token=tokens.access_token,
+ refresh_token=tokens.refresh_token,
+ token_type=tokens.token_type
+ )
+
+
+@router.post("/login", response_model=TokenResponse)
+async def login(
+ credentials: LoginRequest,
+ db: Session = Depends(get_db)
+):
+ """
+ Login with username and password.
+ Returns JWT access and refresh tokens.
+ """
+ user = authenticate_user(db, credentials.username, credentials.password)
+ tokens = create_tokens_for_user(user)
+
+ return tokens
+
+
+@router.get("/me", response_model=UserResponse)
+async def get_current_user_info(
+ current_user: User = Depends(get_current_user)
+):
+ """
+ Get current authenticated user information.
+ Requires valid JWT token in Authorization header.
+ """
+ return UserResponse(
+ id=current_user.id,
+ username=current_user.username,
+ email=current_user.email,
+ is_admin=current_user.is_admin,
+ created_at=current_user.created_at,
+ updated_at=current_user.updated_at
+ )
+
+
+@router.post("/refresh", response_model=TokenResponse)
+async def refresh_access_token(
+ current_user: User = Depends(get_current_user)
+):
+ """
+ Refresh access token using a valid refresh token.
+ Returns new access and refresh tokens.
+ """
+ tokens = create_tokens_for_user(current_user)
+ return tokens
diff --git a/app/routers/items.py b/app/routers/items.py
new file mode 100644
index 0000000..d97edce
--- /dev/null
+++ b/app/routers/items.py
@@ -0,0 +1,90 @@
+"""Map items router for CRUD operations."""
+from typing import List
+from uuid import UUID
+from fastapi import APIRouter, Depends, status
+from sqlalchemy.orm import Session
+
+from app.database import get_db
+from app.dependencies import get_current_user, get_optional_current_user
+from app.models.user import User
+from app.schemas.map_item import MapItemCreate, MapItemUpdate, MapItemResponse
+from app.services import item_service
+from app.services.item_service import geography_to_geojson
+
+
+router = APIRouter(prefix="/api/maps/{map_id}/items", tags=["map-items"])
+
+
+def format_item_response(item) -> dict:
+ """Format map item for response with GeoJSON geometry."""
+ return {
+ "id": str(item.id),
+ "map_id": str(item.map_id),
+ "type": item.type,
+ "geometry": geography_to_geojson(item.geometry),
+ "properties": item.properties,
+ "created_at": item.created_at.isoformat(),
+ "updated_at": item.updated_at.isoformat(),
+ "created_by": str(item.created_by) if item.created_by else None,
+ "updated_by": str(item.updated_by) if item.updated_by else None,
+ }
+
+
+@router.get("", response_model=List[dict])
+async def list_map_items(
+ map_id: UUID,
+ current_user: User = Depends(get_optional_current_user),
+ db: Session = Depends(get_db)
+):
+ """Get all items for a map."""
+ items = item_service.get_map_items(db, map_id, current_user)
+ return [format_item_response(item) for item in items]
+
+
+@router.post("", response_model=dict, status_code=status.HTTP_201_CREATED)
+async def create_map_item(
+ map_id: UUID,
+ item_data: MapItemCreate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Create a new map item."""
+ item = item_service.create_map_item(db, map_id, item_data, current_user)
+ return format_item_response(item)
+
+
+@router.get("/{item_id}", response_model=dict)
+async def get_map_item(
+ map_id: UUID,
+ item_id: UUID,
+ current_user: User = Depends(get_optional_current_user),
+ db: Session = Depends(get_db)
+):
+ """Get a specific map item."""
+ item = item_service.get_map_item_by_id(db, item_id, current_user)
+ return format_item_response(item)
+
+
+@router.patch("/{item_id}", response_model=dict)
+async def update_map_item(
+ map_id: UUID,
+ item_id: UUID,
+ item_data: MapItemUpdate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Update a map item."""
+ item = item_service.update_map_item(db, item_id, item_data, current_user)
+ return format_item_response(item)
+
+
+@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_map_item(
+ map_id: UUID,
+ item_id: UUID,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Delete a map item."""
+ item_service.delete_map_item(db, item_id, current_user)
+ return None
diff --git a/app/routers/maps.py b/app/routers/maps.py
new file mode 100644
index 0000000..42d7aa9
--- /dev/null
+++ b/app/routers/maps.py
@@ -0,0 +1,94 @@
+"""Maps router for CRUD operations."""
+from typing import List
+from uuid import UUID
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+
+from app.database import get_db
+from app.dependencies import get_current_user, get_optional_current_user
+from app.models.user import User
+from app.schemas.map import MapCreate, MapUpdate, MapResponse
+from app.services import map_service
+
+
+router = APIRouter(prefix="/api/maps", tags=["maps"])
+
+
+@router.get("", response_model=List[MapResponse])
+async def list_user_maps(
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Get all maps owned by the current user."""
+ maps = map_service.get_user_maps(db, current_user.id)
+ return maps
+
+
+@router.post("", response_model=MapResponse, status_code=status.HTTP_201_CREATED)
+async def create_map(
+ map_data: MapCreate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Create a new map."""
+ new_map = map_service.create_map(db, map_data, current_user.id)
+ return new_map
+
+
+@router.get("/public", response_model=MapResponse)
+async def get_public_map(db: Session = Depends(get_db)):
+ """Get the default public map (no authentication required)."""
+ public_map = map_service.get_default_public_map(db)
+
+ if not public_map:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="No public map available"
+ )
+
+ return public_map
+
+
+@router.get("/{map_id}", response_model=MapResponse)
+async def get_map(
+ map_id: UUID,
+ current_user: User = Depends(get_optional_current_user),
+ db: Session = Depends(get_db)
+):
+ """Get a specific map by ID. Requires authentication unless it's the public map."""
+ map_obj = map_service.get_map_by_id(db, map_id, current_user)
+ return map_obj
+
+
+@router.patch("/{map_id}", response_model=MapResponse)
+async def update_map(
+ map_id: UUID,
+ map_data: MapUpdate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Update a map. Only the owner or admin can update."""
+ updated_map = map_service.update_map(db, map_id, map_data, current_user)
+ return updated_map
+
+
+@router.delete("/{map_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_map(
+ map_id: UUID,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Delete a map. Only the owner or admin can delete."""
+ map_service.delete_map(db, map_id, current_user)
+ return None
+
+
+@router.post("/{map_id}/set-default-public", response_model=MapResponse)
+async def set_default_public(
+ map_id: UUID,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """Set a map as the default public map. Admin only."""
+ updated_map = map_service.set_default_public_map(db, map_id, current_user)
+ return updated_map
diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/schemas/auth.py b/app/schemas/auth.py
new file mode 100644
index 0000000..af10ae6
--- /dev/null
+++ b/app/schemas/auth.py
@@ -0,0 +1,19 @@
+from pydantic import BaseModel, EmailStr
+
+
+class LoginRequest(BaseModel):
+ """Request schema for user login."""
+ username: str
+ password: str
+
+
+class TokenResponse(BaseModel):
+ """Response schema for authentication tokens."""
+ access_token: str
+ refresh_token: str
+ token_type: str = "bearer"
+
+
+class TokenData(BaseModel):
+ """Schema for JWT token payload data."""
+ user_id: str
diff --git a/app/schemas/map.py b/app/schemas/map.py
new file mode 100644
index 0000000..5261f03
--- /dev/null
+++ b/app/schemas/map.py
@@ -0,0 +1,32 @@
+from pydantic import BaseModel, ConfigDict
+from typing import Optional
+from datetime import datetime
+from uuid import UUID
+
+
+class MapBase(BaseModel):
+ """Base map schema with common attributes."""
+ name: str
+ description: Optional[str] = None
+
+
+class MapCreate(MapBase):
+ """Schema for creating a new map."""
+ pass
+
+
+class MapUpdate(BaseModel):
+ """Schema for updating map information."""
+ name: Optional[str] = None
+ description: Optional[str] = None
+
+
+class MapResponse(MapBase):
+ """Response schema for map data."""
+ id: UUID
+ owner_id: UUID
+ is_default_public: bool
+ created_at: datetime
+ updated_at: datetime
+
+ model_config = ConfigDict(from_attributes=True)
diff --git a/app/schemas/map_item.py b/app/schemas/map_item.py
new file mode 100644
index 0000000..30fb2a5
--- /dev/null
+++ b/app/schemas/map_item.py
@@ -0,0 +1,35 @@
+from pydantic import BaseModel, ConfigDict, Field
+from typing import Optional, Dict, Any
+from datetime import datetime
+from uuid import UUID
+
+
+class MapItemBase(BaseModel):
+ """Base map item schema with common attributes."""
+ type: str = Field(..., description="Item type: cable, wireless_mesh, switch, indoor_ap, outdoor_ap")
+ geometry: Dict[str, Any] = Field(..., description="GeoJSON geometry (Point or LineString)")
+ properties: Dict[str, Any] = Field(default_factory=dict, description="Item-specific properties")
+
+
+class MapItemCreate(MapItemBase):
+ """Schema for creating a new map item."""
+ pass
+
+
+class MapItemUpdate(BaseModel):
+ """Schema for updating map item information."""
+ type: Optional[str] = None
+ geometry: Optional[Dict[str, Any]] = None
+ properties: Optional[Dict[str, Any]] = None
+
+
+class MapItemResponse(MapItemBase):
+ """Response schema for map item data."""
+ id: UUID
+ map_id: UUID
+ created_at: datetime
+ updated_at: datetime
+ created_by: Optional[UUID] = None
+ updated_by: Optional[UUID] = None
+
+ model_config = ConfigDict(from_attributes=True)
diff --git a/app/schemas/share.py b/app/schemas/share.py
new file mode 100644
index 0000000..5c09945
--- /dev/null
+++ b/app/schemas/share.py
@@ -0,0 +1,43 @@
+from pydantic import BaseModel, ConfigDict
+from typing import Optional
+from datetime import datetime
+from uuid import UUID
+
+
+class ShareBase(BaseModel):
+ """Base share schema with common attributes."""
+ access_level: str # 'read_only', 'edit', 'guest_read_only'
+ requires_auth: bool = True
+ expires_at: Optional[datetime] = None
+
+
+class ShareCreate(ShareBase):
+ """Schema for creating a new share link."""
+ pass
+
+
+class ShareUpdate(BaseModel):
+ """Schema for updating share information."""
+ access_level: Optional[str] = None
+ requires_auth: Optional[bool] = None
+ expires_at: Optional[datetime] = None
+
+
+class ShareResponse(ShareBase):
+ """Response schema for share data."""
+ id: UUID
+ map_id: UUID
+ share_token: str
+ created_at: datetime
+ created_by: Optional[UUID] = None
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class ShareValidateResponse(BaseModel):
+ """Response schema for share token validation."""
+ valid: bool
+ access_level: Optional[str] = None
+ map_id: Optional[UUID] = None
+ requires_auth: bool = False
+ expired: bool = False
diff --git a/app/schemas/user.py b/app/schemas/user.py
new file mode 100644
index 0000000..82bf2df
--- /dev/null
+++ b/app/schemas/user.py
@@ -0,0 +1,39 @@
+from pydantic import BaseModel, EmailStr, ConfigDict
+from typing import Optional
+from datetime import datetime
+from uuid import UUID
+
+
+class UserBase(BaseModel):
+ """Base user schema with common attributes."""
+ username: str
+ email: EmailStr
+
+
+class UserCreate(UserBase):
+ """Schema for creating a new user."""
+ password: str
+
+
+class UserUpdate(BaseModel):
+ """Schema for updating user information."""
+ username: Optional[str] = None
+ email: Optional[EmailStr] = None
+ password: Optional[str] = None
+
+
+class UserResponse(UserBase):
+ """Response schema for user data (without sensitive info)."""
+ id: UUID
+ is_admin: bool
+ created_at: datetime
+ updated_at: datetime
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class UserWithToken(UserResponse):
+ """User response with authentication tokens."""
+ access_token: str
+ refresh_token: str
+ token_type: str = "bearer"
diff --git a/app/services/__init__.py b/app/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/services/auth_service.py b/app/services/auth_service.py
new file mode 100644
index 0000000..d04bd5a
--- /dev/null
+++ b/app/services/auth_service.py
@@ -0,0 +1,106 @@
+from sqlalchemy.orm import Session
+from fastapi import HTTPException, status
+from app.models.user import User
+from app.schemas.user import UserCreate
+from app.schemas.auth import TokenResponse
+from app.utils.password import hash_password, verify_password
+from app.utils.security import create_access_token, create_refresh_token
+from app.config import settings
+
+
+def authenticate_user(db: Session, username: str, password: str) -> User:
+ """
+ Authenticate a user by username and password.
+
+ Args:
+ db: Database session
+ username: Username
+ password: Plain text password
+
+ Returns:
+ User object if authentication successful
+
+ Raises:
+ HTTPException: If authentication fails
+ """
+ user = db.query(User).filter(User.username == username).first()
+
+ if not user or not verify_password(password, user.password_hash):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ return user
+
+
+def create_user(db: Session, user_data: UserCreate) -> User:
+ """
+ Create a new user.
+
+ Args:
+ db: Database session
+ user_data: User creation data
+
+ Returns:
+ Created user object
+
+ Raises:
+ HTTPException: If username or email already exists
+ """
+ # Check if registration is allowed
+ if not settings.ALLOW_REGISTRATION:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="User registration is currently disabled"
+ )
+
+ # Check if username already exists
+ if db.query(User).filter(User.username == user_data.username).first():
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Username already registered"
+ )
+
+ # Check if email already exists
+ if db.query(User).filter(User.email == user_data.email).first():
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Email already registered"
+ )
+
+ # Create new user
+ hashed_password = hash_password(user_data.password)
+ db_user = User(
+ username=user_data.username,
+ email=user_data.email,
+ password_hash=hashed_password,
+ is_admin=False
+ )
+
+ db.add(db_user)
+ db.commit()
+ db.refresh(db_user)
+
+ return db_user
+
+
+def create_tokens_for_user(user: User) -> TokenResponse:
+ """
+ Create access and refresh tokens for a user.
+
+ Args:
+ user: User object
+
+ Returns:
+ TokenResponse with access and refresh tokens
+ """
+ access_token = create_access_token(str(user.id))
+ refresh_token = create_refresh_token(str(user.id))
+
+ return TokenResponse(
+ access_token=access_token,
+ refresh_token=refresh_token,
+ token_type="bearer"
+ )
diff --git a/app/services/item_service.py b/app/services/item_service.py
new file mode 100644
index 0000000..83a936e
--- /dev/null
+++ b/app/services/item_service.py
@@ -0,0 +1,227 @@
+"""Map item service for business logic."""
+from typing import List, Optional
+from uuid import UUID
+from sqlalchemy.orm import Session
+from fastapi import HTTPException, status
+from geoalchemy2.shape import from_shape, to_shape
+from shapely.geometry import shape, Point, LineString
+import json
+
+from app.models.map_item import MapItem
+from app.models.user import User
+from app.schemas.map_item import MapItemCreate, MapItemUpdate
+from app.services.map_service import get_map_by_id
+
+
+def get_map_items(db: Session, map_id: UUID, user: Optional[User] = None) -> List[MapItem]:
+ """Get all items for a map."""
+ # Verify user has access to the map
+ get_map_by_id(db, map_id, user)
+
+ items = db.query(MapItem).filter(MapItem.map_id == map_id).all()
+ return items
+
+
+def get_map_item_by_id(db: Session, item_id: UUID, user: Optional[User] = None) -> MapItem:
+ """Get a map item by ID."""
+ item = db.query(MapItem).filter(MapItem.id == item_id).first()
+
+ if not item:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Map item not found"
+ )
+
+ # Verify user has access to the map
+ get_map_by_id(db, item.map_id, user)
+
+ return item
+
+
+def geojson_to_geography(geojson: dict) -> str:
+ """Convert GeoJSON geometry to PostGIS geography WKT."""
+ geom = shape(geojson)
+
+ # Ensure coordinates are in the correct format for PostGIS (lon, lat)
+ if isinstance(geom, Point):
+ return f'SRID=4326;POINT({geom.x} {geom.y})'
+ elif isinstance(geom, LineString):
+ coords = ', '.join([f'{x} {y}' for x, y in geom.coords])
+ return f'SRID=4326;LINESTRING({coords})'
+ else:
+ raise ValueError(f"Unsupported geometry type: {type(geom)}")
+
+
+def geography_to_geojson(geography) -> dict:
+ """Convert PostGIS geography to GeoJSON."""
+ geom = to_shape(geography)
+ return json.loads(json.dumps(geom.__geo_interface__))
+
+
+def create_map_item(db: Session, map_id: UUID, item_data: MapItemCreate, user: User) -> MapItem:
+ """Create a new map item."""
+ # Verify user has access to the map
+ get_map_by_id(db, map_id, user)
+
+ # Convert GeoJSON to PostGIS geography
+ geometry_wkt = geojson_to_geography(item_data.geometry)
+
+ item = MapItem(
+ map_id=map_id,
+ type=item_data.type,
+ geometry=geometry_wkt,
+ properties=item_data.properties,
+ created_by=user.id,
+ updated_by=user.id
+ )
+
+ db.add(item)
+ db.commit()
+ db.refresh(item)
+
+ # If this is a cable with device connections, update device port tracking
+ if item.type == 'cable':
+ start_device_id = item.properties.get('start_device_id')
+ end_device_id = item.properties.get('end_device_id')
+
+ print(f"Cable created: start_device_id={start_device_id}, end_device_id={end_device_id}")
+
+ if start_device_id:
+ print(f"Updating port connections for start device: {start_device_id}")
+ update_device_connections(db, UUID(start_device_id), item.id)
+ if end_device_id:
+ print(f"Updating port connections for end device: {end_device_id}")
+ update_device_connections(db, UUID(end_device_id), item.id)
+
+ return item
+
+
+def update_device_connections(db: Session, device_id: UUID, cable_id: UUID) -> None:
+ """Add cable connection to device's connections array."""
+ print(f"update_device_connections called: device_id={device_id}, cable_id={cable_id}")
+ device = db.query(MapItem).filter(MapItem.id == device_id).first()
+ if not device:
+ print(f"Device not found: {device_id}")
+ return
+
+ # Create a mutable copy of properties
+ properties = dict(device.properties) if device.properties else {}
+ connections = properties.get('connections', [])
+ port_count = properties.get('port_count', 0)
+
+ print(f"Device {device_id}: port_count={port_count}, current_connections={len(connections)}")
+
+ # Find next available port
+ used_ports = {conn.get('port_number') for conn in connections if isinstance(conn, dict)}
+ next_port = 1
+ while next_port in used_ports and next_port <= port_count:
+ next_port += 1
+
+ # Only add if there's an available port
+ if next_port <= port_count:
+ connections.append({
+ 'cable_id': str(cable_id),
+ 'port_number': next_port
+ })
+ properties['connections'] = connections
+
+ # Mark the column as modified so SQLAlchemy detects the change
+ from sqlalchemy.orm.attributes import flag_modified
+ device.properties = properties
+ flag_modified(device, 'properties')
+
+ db.commit()
+ db.refresh(device)
+ print(f"Added connection to port {next_port}. Total connections now: {len(connections)}")
+ print(f"Device properties after update: {device.properties}")
+ else:
+ print(f"No available ports! Port count: {port_count}, used: {len(connections)}")
+
+
+def update_map_item(db: Session, item_id: UUID, item_data: MapItemUpdate, user: User) -> MapItem:
+ """Update a map item."""
+ item = get_map_item_by_id(db, item_id, user)
+
+ # Update fields if provided
+ if item_data.type is not None:
+ item.type = item_data.type
+ if item_data.geometry is not None:
+ item.geometry = geojson_to_geography(item_data.geometry)
+ if item_data.properties is not None:
+ item.properties = item_data.properties
+
+ item.updated_by = user.id
+
+ db.commit()
+ db.refresh(item)
+
+ return item
+
+
+def delete_map_item(db: Session, item_id: UUID, user: User) -> None:
+ """Delete a map item."""
+ item = get_map_item_by_id(db, item_id, user)
+
+ # If deleting a cable, remove it from device connections
+ if item.type == 'cable':
+ start_device_id = item.properties.get('start_device_id')
+ end_device_id = item.properties.get('end_device_id')
+
+ if start_device_id:
+ remove_device_connection(db, UUID(start_device_id), item.id)
+ if end_device_id:
+ remove_device_connection(db, UUID(end_device_id), item.id)
+
+ # If deleting an AP, delete all associated wireless mesh links
+ if item.type in ['indoor_ap', 'outdoor_ap']:
+ print(f"Deleting AP {item.id}, checking for wireless mesh links...")
+ # Find all wireless mesh links connected to this AP
+ wireless_meshes = db.query(MapItem).filter(
+ MapItem.map_id == item.map_id,
+ MapItem.type == 'wireless_mesh'
+ ).all()
+
+ for mesh in wireless_meshes:
+ start_ap_id = mesh.properties.get('start_ap_id')
+ end_ap_id = mesh.properties.get('end_ap_id')
+
+ if start_ap_id == str(item.id) or end_ap_id == str(item.id):
+ print(f"Deleting wireless mesh {mesh.id} connected to AP {item.id}")
+ db.delete(mesh)
+
+ db.delete(item)
+ db.commit()
+
+
+def remove_device_connection(db: Session, device_id: UUID, cable_id: UUID) -> None:
+ """Remove cable connection from device's connections array."""
+ print(f"remove_device_connection called: device_id={device_id}, cable_id={cable_id}")
+ device = db.query(MapItem).filter(MapItem.id == device_id).first()
+ if not device:
+ print(f"Device not found: {device_id}")
+ return
+
+ # Create a mutable copy of properties
+ properties = dict(device.properties) if device.properties else {}
+ connections = properties.get('connections', [])
+
+ print(f"Before removal: {len(connections)} connections")
+
+ # Filter out the cable connection
+ connections = [
+ conn for conn in connections
+ if isinstance(conn, dict) and conn.get('cable_id') != str(cable_id)
+ ]
+
+ print(f"After removal: {len(connections)} connections")
+
+ properties['connections'] = connections
+
+ # Mark the column as modified so SQLAlchemy detects the change
+ from sqlalchemy.orm.attributes import flag_modified
+ device.properties = properties
+ flag_modified(device, 'properties')
+
+ db.commit()
+ db.refresh(device)
+ print(f"Removed cable connection. Device now has {len(connections)} connections")
diff --git a/app/services/map_service.py b/app/services/map_service.py
new file mode 100644
index 0000000..c4b3c9f
--- /dev/null
+++ b/app/services/map_service.py
@@ -0,0 +1,103 @@
+"""Map service for business logic."""
+from typing import List, Optional
+from uuid import UUID
+from sqlalchemy.orm import Session
+from fastapi import HTTPException, status
+
+from app.models.map import Map
+from app.models.user import User
+from app.schemas.map import MapCreate, MapUpdate
+
+
+def get_user_maps(db: Session, user_id: UUID) -> List[Map]:
+ """Get all maps owned by a user."""
+ return db.query(Map).filter(Map.owner_id == user_id).order_by(Map.updated_at.desc()).all()
+
+
+def get_map_by_id(db: Session, map_id: UUID, user: Optional[User] = None) -> Map:
+ """Get a map by ID with optional authorization check."""
+ map_obj = db.query(Map).filter(Map.id == map_id).first()
+
+ if not map_obj:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Map not found"
+ )
+
+ # If user is provided, check authorization
+ if user:
+ if map_obj.owner_id != user.id and not user.is_admin:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="You don't have permission to access this map"
+ )
+
+ return map_obj
+
+
+def get_default_public_map(db: Session) -> Optional[Map]:
+ """Get the default public map (first map with is_default_public=True)."""
+ return db.query(Map).filter(Map.is_default_public == True).first()
+
+
+def create_map(db: Session, map_data: MapCreate, user_id: UUID) -> Map:
+ """Create a new map."""
+ map_obj = Map(
+ name=map_data.name,
+ description=map_data.description,
+ owner_id=user_id,
+ is_default_public=False
+ )
+
+ db.add(map_obj)
+ db.commit()
+ db.refresh(map_obj)
+
+ return map_obj
+
+
+def update_map(db: Session, map_id: UUID, map_data: MapUpdate, user: User) -> Map:
+ """Update a map. Only owner or admin can update."""
+ map_obj = get_map_by_id(db, map_id, user)
+
+ # Update fields if provided
+ if map_data.name is not None:
+ map_obj.name = map_data.name
+ if map_data.description is not None:
+ map_obj.description = map_data.description
+
+ db.commit()
+ db.refresh(map_obj)
+
+ return map_obj
+
+
+def delete_map(db: Session, map_id: UUID, user: User) -> None:
+ """Delete a map. Only owner or admin can delete."""
+ map_obj = get_map_by_id(db, map_id, user)
+
+ db.delete(map_obj)
+ db.commit()
+
+
+def set_default_public_map(db: Session, map_id: UUID, user: User) -> Map:
+ """Set a map as the default public map. Only admins can do this."""
+ if not user.is_admin:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Only administrators can set the default public map"
+ )
+
+ # Get the map (admin check is done in get_map_by_id)
+ map_obj = get_map_by_id(db, map_id, user)
+
+ # Unset any existing default public maps
+ db.query(Map).filter(Map.is_default_public == True).update({"is_default_public": False})
+
+ # Set this map as default public
+ map_obj.is_default_public = True
+
+ db.commit()
+ db.refresh(map_obj)
+
+ return map_obj
diff --git a/app/utils/__init__.py b/app/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/utils/password.py b/app/utils/password.py
new file mode 100644
index 0000000..1196c9f
--- /dev/null
+++ b/app/utils/password.py
@@ -0,0 +1,31 @@
+from passlib.context import CryptContext
+
+# Password hashing context using bcrypt
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+def hash_password(password: str) -> str:
+ """
+ Hash a plain password using bcrypt.
+
+ Args:
+ password: Plain text password
+
+ Returns:
+ Hashed password string
+ """
+ return pwd_context.hash(password)
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """
+ Verify a plain password against a hashed password.
+
+ Args:
+ plain_password: Plain text password to verify
+ hashed_password: Hashed password from database
+
+ Returns:
+ True if password matches, False otherwise
+ """
+ return pwd_context.verify(plain_password, hashed_password)
diff --git a/app/utils/security.py b/app/utils/security.py
new file mode 100644
index 0000000..c82e645
--- /dev/null
+++ b/app/utils/security.py
@@ -0,0 +1,69 @@
+from datetime import datetime, timedelta
+from typing import Optional
+from jose import jwt
+from app.config import settings
+
+
+def create_access_token(user_id: str, expires_delta: Optional[timedelta] = None) -> str:
+ """
+ Create a JWT access token.
+
+ Args:
+ user_id: User ID to encode in the token
+ expires_delta: Optional custom expiration time delta
+
+ Returns:
+ Encoded JWT token string
+ """
+ if expires_delta:
+ expire = datetime.utcnow() + expires_delta
+ else:
+ expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+
+ to_encode = {
+ "sub": user_id,
+ "exp": expire,
+ "type": "access"
+ }
+
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
+ return encoded_jwt
+
+
+def create_refresh_token(user_id: str) -> str:
+ """
+ Create a JWT refresh token with longer expiration.
+
+ Args:
+ user_id: User ID to encode in the token
+
+ Returns:
+ Encoded JWT refresh token string
+ """
+ expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
+
+ to_encode = {
+ "sub": user_id,
+ "exp": expire,
+ "type": "refresh"
+ }
+
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
+ return encoded_jwt
+
+
+def decode_token(token: str) -> dict:
+ """
+ Decode and verify a JWT token.
+
+ Args:
+ token: JWT token string
+
+ Returns:
+ Decoded token payload
+
+ Raises:
+ JWTError: If token is invalid or expired
+ """
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
+ return payload
diff --git a/app/websocket/__init__.py b/app/websocket/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/public/.gitignore b/public/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/public/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/public/README.md b/public/README.md
new file mode 100644
index 0000000..d2e7761
--- /dev/null
+++ b/public/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/public/eslint.config.js b/public/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/public/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..25721cd
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ ISP Wiremap
+
+
+
+
+
+
diff --git a/public/package-lock.json b/public/package-lock.json
new file mode 100644
index 0000000..131944a
--- /dev/null
+++ b/public/package-lock.json
@@ -0,0 +1,4346 @@
+{
+ "name": "public",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "public",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.12",
+ "@types/leaflet": "^1.9.21",
+ "axios": "^1.13.2",
+ "leaflet": "^1.9.4",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-leaflet": "^5.0.0",
+ "react-router-dom": "^7.10.1",
+ "zustand": "^5.0.9"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@tailwindcss/postcss": "^4.1.18",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.22",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.18",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "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/eslint-utils/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,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.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,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@react-leaflet/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
+ "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.18"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
+ "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.18",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.18",
+ "@tailwindcss/oxide-darwin-x64": "4.1.18",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.18",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.18",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.18",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
+ "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
+ "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
+ "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
+ "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
+ "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
+ "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
+ "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
+ "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
+ "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
+ "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.0",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
+ "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
+ "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
+ "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.1.18",
+ "@tailwindcss/oxide": "4.1.18",
+ "postcss": "^8.4.41",
+ "tailwindcss": "4.1.18"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz",
+ "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz",
+ "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.3.tgz",
+ "integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
+ "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.49.0",
+ "@typescript-eslint/type-utils": "8.49.0",
+ "@typescript-eslint/utils": "8.49.0",
+ "@typescript-eslint/visitor-keys": "8.49.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.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.49.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz",
+ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.49.0",
+ "@typescript-eslint/types": "8.49.0",
+ "@typescript-eslint/typescript-estree": "8.49.0",
+ "@typescript-eslint/visitor-keys": "8.49.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",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz",
+ "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.49.0",
+ "@typescript-eslint/types": "^8.49.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": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz",
+ "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.49.0",
+ "@typescript-eslint/visitor-keys": "8.49.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/tsconfig-utils": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz",
+ "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz",
+ "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.49.0",
+ "@typescript-eslint/typescript-estree": "8.49.0",
+ "@typescript-eslint/utils": "8.49.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.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",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz",
+ "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==",
+ "dev": true,
+ "license": "MIT",
+ "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.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz",
+ "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.49.0",
+ "@typescript-eslint/tsconfig-utils": "8.49.0",
+ "@typescript-eslint/types": "8.49.0",
+ "@typescript-eslint/visitor-keys": "8.49.0",
+ "debug": "^4.3.4",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@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,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz",
+ "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.49.0",
+ "@typescript-eslint/types": "8.49.0",
+ "@typescript-eslint/typescript-estree": "8.49.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",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz",
+ "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.49.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "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,
+ "license": "MIT",
+ "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,
+ "license": "MIT",
+ "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-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/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,
+ "license": "Python-2.0"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.22",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
+ "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.27.0",
+ "caniuse-lite": "^1.0.30001754",
+ "fraction.js": "^5.3.4",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.6",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
+ "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001760",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
+ "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.4",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
+ "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.1",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.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,
+ "license": "BSD-3-Clause",
+ "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,
+ "license": "BSD-2-Clause",
+ "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,
+ "license": "BSD-2-Clause",
+ "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,
+ "license": "BSD-2-Clause",
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-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,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "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": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-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,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/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,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "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,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "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/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "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,
+ "license": "MIT",
+ "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,
+ "license": "MIT",
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "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"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "node_modules/react-leaflet": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
+ "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^3.0.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
+ "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
+ "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.10.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
+ "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz",
+ "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "@typescript-eslint/typescript-estree": "8.49.0",
+ "@typescript-eslint/utils": "8.49.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",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
+ "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.2.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
+ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "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,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
+ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",
+ "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/public/package.json b/public/package.json
new file mode 100644
index 0000000..bc3612b
--- /dev/null
+++ b/public/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "public",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.12",
+ "@types/leaflet": "^1.9.21",
+ "axios": "^1.13.2",
+ "leaflet": "^1.9.4",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-leaflet": "^5.0.0",
+ "react-router-dom": "^7.10.1",
+ "zustand": "^5.0.9"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@tailwindcss/postcss": "^4.1.18",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.22",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.18",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4"
+ }
+}
diff --git a/public/postcss.config.js b/public/postcss.config.js
new file mode 100644
index 0000000..1c87846
--- /dev/null
+++ b/public/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ autoprefixer: {},
+ },
+}
diff --git a/public/public/vite.svg b/public/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/src/App.css b/public/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/public/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/public/src/App.tsx b/public/src/App.tsx
new file mode 100644
index 0000000..1ce6d41
--- /dev/null
+++ b/public/src/App.tsx
@@ -0,0 +1,25 @@
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import { Login } from './components/auth/Login';
+import { ProtectedRoute } from './components/auth/ProtectedRoute';
+import { Dashboard } from './pages/Dashboard';
+
+function App() {
+ return (
+
+
+ } />
+
+
+
+ }
+ />
+ } />
+
+
+ );
+}
+
+export default App;
diff --git a/public/src/assets/react.svg b/public/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/public/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/src/components/auth/Login.tsx b/public/src/components/auth/Login.tsx
new file mode 100644
index 0000000..a815401
--- /dev/null
+++ b/public/src/components/auth/Login.tsx
@@ -0,0 +1,78 @@
+import { useState, FormEvent } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuthStore } from '../../stores/authStore';
+
+export function Login() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const { login, isLoading, error, clearError } = useAuthStore();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ clearError();
+
+ try {
+ await login(username, password);
+ navigate('/');
+ } catch (err) {
+ // Error is handled by the store
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/public/src/components/auth/ProtectedRoute.tsx b/public/src/components/auth/ProtectedRoute.tsx
new file mode 100644
index 0000000..35695ee
--- /dev/null
+++ b/public/src/components/auth/ProtectedRoute.tsx
@@ -0,0 +1,31 @@
+import { useEffect } from 'react';
+import { Navigate } from 'react-router-dom';
+import { useAuthStore } from '../../stores/authStore';
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+export function ProtectedRoute({ children }: ProtectedRouteProps) {
+ const { isAuthenticated, loadUser, isLoading } = useAuthStore();
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ loadUser();
+ }
+ }, [isAuthenticated, loadUser]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return <>{children}>;
+}
diff --git a/public/src/components/layout/Layout.tsx b/public/src/components/layout/Layout.tsx
new file mode 100644
index 0000000..df0a849
--- /dev/null
+++ b/public/src/components/layout/Layout.tsx
@@ -0,0 +1,50 @@
+import { ReactNode } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuthStore } from '../../stores/authStore';
+
+interface LayoutProps {
+ children: ReactNode;
+}
+
+export function Layout({ children }: LayoutProps) {
+ const { user, logout } = useAuthStore();
+ const navigate = useNavigate();
+
+ const handleLogout = () => {
+ logout();
+ navigate('/login');
+ };
+
+ return (
+
+ );
+}
diff --git a/public/src/components/map/DrawingHandler.tsx b/public/src/components/map/DrawingHandler.tsx
new file mode 100644
index 0000000..87c9bbf
--- /dev/null
+++ b/public/src/components/map/DrawingHandler.tsx
@@ -0,0 +1,338 @@
+import { useEffect, useState } from 'react';
+import { useMapEvents, Polyline, Marker } from 'react-leaflet';
+import L from 'leaflet';
+import { useDrawingStore } from '../../stores/drawingStore';
+import { mapItemService } from '../../services/mapItemService';
+import { CABLE_COLORS, type CableType } from '../../types/mapItem';
+
+interface DrawingHandlerProps {
+ mapId: string;
+ onItemCreated: () => void;
+}
+
+export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) {
+ const { activeTool, isDrawing, drawingPoints, setIsDrawing, addDrawingPoint, resetDrawing, setActiveTool } =
+ useDrawingStore();
+ const [cursorPosition, setCursorPosition] = useState<[number, number] | null>(null);
+ const [allItems, setAllItems] = useState([]);
+ const [startDeviceId, setStartDeviceId] = useState(null);
+ const [endDeviceId, setEndDeviceId] = useState(null);
+
+ const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool);
+ const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap'].includes(activeTool);
+ const isWirelessTool = activeTool === 'wireless_mesh';
+
+ // Load all map items for snapping
+ useEffect(() => {
+ const loadItems = async () => {
+ try {
+ const items = await mapItemService.getMapItems(mapId);
+ setAllItems(items);
+ } catch (error) {
+ console.error('Failed to load items for snapping:', error);
+ }
+ };
+ loadItems();
+ }, [mapId]);
+
+ // Find nearby device for snapping
+ const findNearbyDevice = (lat: number, lng: number, radiusMeters = 5): any | null => {
+ const devices = allItems.filter(item =>
+ ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) &&
+ item.geometry.type === 'Point'
+ );
+
+ for (const device of devices) {
+ const [deviceLng, deviceLat] = device.geometry.coordinates;
+ const distance = map.distance([lat, lng], [deviceLat, deviceLng]);
+
+ if (distance <= radiusMeters) {
+ return device;
+ }
+ }
+
+ return null;
+ };
+
+ // Check if device has available ports
+ const hasAvailablePorts = (device: any): boolean => {
+ const portCount = device.properties.port_count || 0;
+ const usedPorts = device.properties.connections?.length || 0;
+ return usedPorts < portCount;
+ };
+
+ // Count wireless mesh connections for an AP
+ const getWirelessMeshCount = (apId: string): number => {
+ const meshLinks = allItems.filter(item =>
+ item.type === 'wireless_mesh' &&
+ (item.properties.start_ap_id === apId || item.properties.end_ap_id === apId)
+ );
+ return meshLinks.length;
+ };
+
+ const map = useMapEvents({
+ click: async (e) => {
+ if (activeTool === 'select') return;
+
+ let { lat, lng } = e.latlng;
+ let clickedDevice = null;
+
+ // Check for nearby device when drawing cables
+ if (isCableTool && map) {
+ clickedDevice = findNearbyDevice(lat, lng);
+ if (clickedDevice) {
+ // Check if device has available ports
+ if (!hasAvailablePorts(clickedDevice)) {
+ const portCount = clickedDevice.properties.port_count || 0;
+ const usedPorts = clickedDevice.properties.connections?.length || 0;
+ const deviceName = clickedDevice.properties.name || clickedDevice.type;
+ alert(`${deviceName} has no available ports (${usedPorts}/${portCount} ports used). Please select a different device or increase the port count.`);
+ return;
+ }
+
+ // Snap to device center
+ const [deviceLng, deviceLat] = clickedDevice.geometry.coordinates;
+ lat = deviceLat;
+ lng = deviceLng;
+
+ // Track device connection
+ if (!isDrawing) {
+ setStartDeviceId(clickedDevice.id);
+ } else {
+ setEndDeviceId(clickedDevice.id);
+ }
+ }
+ }
+
+ // Device placement - single click
+ if (isDeviceTool) {
+ try {
+ await mapItemService.createMapItem(mapId, {
+ type: activeTool as any,
+ geometry: {
+ type: 'Point',
+ coordinates: [lng, lat],
+ },
+ properties: {
+ name: `${activeTool} at ${lat.toFixed(4)}, ${lng.toFixed(4)}`,
+ port_count: activeTool === 'switch' ? 5 : activeTool === 'outdoor_ap' ? 1 : 4,
+ connections: [],
+ },
+ });
+ onItemCreated();
+ // Reload items for snapping
+ const items = await mapItemService.getMapItems(mapId);
+ setAllItems(items);
+ } catch (error) {
+ console.error('Failed to create device:', error);
+ }
+ return;
+ }
+
+ // Cable drawing - multi-point
+ if (isCableTool) {
+ if (!isDrawing) {
+ setIsDrawing(true);
+ addDrawingPoint([lat, lng]);
+ } else {
+ addDrawingPoint([lat, lng]);
+ }
+ }
+
+ // Wireless mesh - connect AP to AP only
+ if (isWirelessTool) {
+ // Must click on an AP
+ const ap = findNearbyDevice(lat, lng, 5);
+ if (!ap || !['indoor_ap', 'outdoor_ap'].includes(ap.type)) {
+ alert('Wireless mesh can only connect between Access Points. Please click on an AP.');
+ return;
+ }
+
+ // Check wireless mesh connection limit (max 4 per AP)
+ const meshCount = getWirelessMeshCount(ap.id);
+ if (meshCount >= 4) {
+ const apName = ap.properties.name || ap.type;
+ alert(`${apName} already has the maximum of 4 wireless mesh connections. Please select a different AP.`);
+ return;
+ }
+
+ if (!isDrawing) {
+ // First AP clicked
+ setIsDrawing(true);
+ setStartDeviceId(ap.id);
+ const [apLng, apLat] = ap.geometry.coordinates;
+ addDrawingPoint([apLat, apLng]);
+ } else if (drawingPoints.length === 1) {
+ // Second AP clicked - check if it's different from first
+ if (ap.id === startDeviceId) {
+ alert('Cannot create wireless mesh to the same Access Point. Please select a different AP.');
+ return;
+ }
+
+ // Second AP clicked - finish immediately with both points
+ const [apLng, apLat] = ap.geometry.coordinates;
+ const secondPoint: [number, number] = [apLat, apLng];
+ await finishWirelessMesh(ap.id, secondPoint);
+ }
+ }
+ },
+
+ mousemove: (e) => {
+ // Update cursor position for preview line
+ if (isDrawing && (isCableTool || isWirelessTool)) {
+ setCursorPosition([e.latlng.lat, e.latlng.lng]);
+ }
+ },
+
+ contextmenu: async (e) => {
+ // Right-click to finish cable drawing
+ if (isCableTool && isDrawing && drawingPoints.length >= 2) {
+ e.originalEvent.preventDefault();
+ e.originalEvent.stopPropagation();
+ await finishCable();
+ return false;
+ }
+
+ if (!isDrawing) {
+ e.originalEvent.preventDefault();
+ }
+ },
+ });
+
+ useEffect(() => {
+ // Listen for Escape key globally
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && isDrawing) {
+ resetDrawing();
+ setCursorPosition(null);
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [isDrawing, resetDrawing]);
+
+ const finishCable = async () => {
+ if (drawingPoints.length < 2) return;
+
+ try {
+ const cableType = activeTool as CableType;
+ await mapItemService.createMapItem(mapId, {
+ type: 'cable',
+ geometry: {
+ type: 'LineString',
+ coordinates: drawingPoints.map(([lat, lng]) => [lng, lat]),
+ },
+ properties: {
+ cable_type: cableType,
+ name: `${cableType} cable`,
+ start_device_id: startDeviceId,
+ end_device_id: endDeviceId,
+ },
+ });
+ onItemCreated();
+ resetDrawing();
+ setCursorPosition(null);
+ setStartDeviceId(null);
+ setEndDeviceId(null);
+ // Reload items
+ const items = await mapItemService.getMapItems(mapId);
+ setAllItems(items);
+ } catch (error) {
+ console.error('Failed to create cable:', error);
+ }
+ };
+
+ const finishWirelessMesh = async (endApId: string, secondPoint: [number, number]) => {
+ if (drawingPoints.length !== 1) {
+ console.error('Wireless mesh requires exactly 1 starting point');
+ return;
+ }
+
+ try {
+ // Create line with first point from state and second point from parameter
+ const coordinates = [
+ [drawingPoints[0][1], drawingPoints[0][0]], // First point: [lng, lat]
+ [secondPoint[1], secondPoint[0]], // Second point: [lng, lat]
+ ];
+
+ await mapItemService.createMapItem(mapId, {
+ type: 'wireless_mesh',
+ geometry: {
+ type: 'LineString',
+ coordinates: coordinates,
+ },
+ properties: {
+ name: 'Wireless mesh link',
+ start_ap_id: startDeviceId,
+ end_ap_id: endApId,
+ },
+ });
+ onItemCreated();
+ // Reset drawing state but keep the wireless mesh tool active
+ resetDrawing();
+ setCursorPosition(null);
+ setStartDeviceId(null);
+ setEndDeviceId(null);
+ // Reload items
+ const items = await mapItemService.getMapItems(mapId);
+ setAllItems(items);
+ } catch (error) {
+ console.error('Failed to create wireless mesh:', error);
+ // Reset on error too
+ resetDrawing();
+ setCursorPosition(null);
+ setStartDeviceId(null);
+ setEndDeviceId(null);
+ }
+ };
+
+ // Render drawing preview
+ if (isDrawing && drawingPoints.length > 0) {
+ const color = isCableTool
+ ? CABLE_COLORS[activeTool as CableType]
+ : isWirelessTool
+ ? '#10B981'
+ : '#6B7280';
+
+ const dashArray = isWirelessTool ? '10, 10' : undefined;
+
+ // Create preview line from last point to cursor
+ const previewPositions = cursorPosition
+ ? [...drawingPoints, cursorPosition]
+ : drawingPoints;
+
+ return (
+ <>
+ {/* Main line connecting all points */}
+ {drawingPoints.length > 1 && (
+
+ )}
+
+ {/* Preview line from last point to cursor */}
+ {cursorPosition && drawingPoints.length > 0 && (
+
+ )}
+
+ {/* Markers at each point */}
+ {drawingPoints.map((point, idx) => (
+
+ ))}
+ >
+ );
+ }
+
+ return null;
+}
diff --git a/public/src/components/map/ItemContextMenu.tsx b/public/src/components/map/ItemContextMenu.tsx
new file mode 100644
index 0000000..ab9b027
--- /dev/null
+++ b/public/src/components/map/ItemContextMenu.tsx
@@ -0,0 +1,387 @@
+import { useState, useEffect, useRef } from 'react';
+import { mapItemService } from '../../services/mapItemService';
+
+interface ItemContextMenuProps {
+ item: any;
+ position: { x: number; y: number };
+ onClose: () => void;
+ onUpdate: () => void;
+}
+
+export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemContextMenuProps) {
+ const [showRenameDialog, setShowRenameDialog] = useState(false);
+ const [showNotesDialog, setShowNotesDialog] = useState(false);
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ const [showPortConfigDialog, setShowPortConfigDialog] = useState(false);
+ const [newName, setNewName] = useState(item.properties.name || '');
+ const [notes, setNotes] = useState(item.properties.notes || '');
+ const [imageData, setImageData] = useState(item.properties.image || null);
+ const [portCount, setPortCount] = useState(item.properties.port_count || 5);
+ const [deleteConnectedCables, setDeleteConnectedCables] = useState(false);
+ const menuRef = useRef(null);
+ const fileInputRef = useRef(null);
+
+ const isSwitch = item.type === 'switch';
+ const isDevice = ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type);
+ const hasConnections = item.properties.connections && item.properties.connections.length > 0;
+
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ onClose();
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [onClose]);
+
+ const handleDelete = async () => {
+ try {
+ // If device with connections and user wants to delete connected cables
+ if (isDevice && deleteConnectedCables && hasConnections) {
+ // First delete all connected cables
+ const { mapItemService: itemService } = await import('../../services/mapItemService');
+ const allItems = await itemService.getMapItems(item.map_id);
+
+ // Find all cables connected to this device
+ const connectedCableIds = item.properties.connections.map((conn: any) => conn.cable_id);
+ const cablesToDelete = allItems.filter((i: any) =>
+ i.type === 'cable' && connectedCableIds.includes(i.id)
+ );
+
+ // Delete each cable
+ for (const cable of cablesToDelete) {
+ await itemService.deleteMapItem(item.map_id, cable.id);
+ }
+ }
+
+ // Delete the device/item itself
+ await mapItemService.deleteMapItem(item.map_id, item.id);
+ onUpdate();
+ onClose();
+ } catch (error) {
+ console.error('Failed to delete item:', error);
+ alert('Failed to delete item');
+ }
+ };
+
+ const handleRename = async () => {
+ try {
+ await mapItemService.updateMapItem(item.map_id, item.id, {
+ properties: {
+ ...item.properties,
+ name: newName,
+ },
+ });
+ onUpdate();
+ setShowRenameDialog(false);
+ onClose();
+ } catch (error) {
+ console.error('Failed to rename item:', error);
+ alert('Failed to rename item');
+ }
+ };
+
+ const handleImageUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Check file size (max 2MB)
+ if (file.size > 2 * 1024 * 1024) {
+ alert('Image too large. Please use an image smaller than 2MB.');
+ return;
+ }
+
+ // Check file type
+ if (!file.type.startsWith('image/')) {
+ alert('Please select an image file.');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ setImageData(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleRemoveImage = () => {
+ setImageData(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ const handleSaveNotes = async () => {
+ try {
+ await mapItemService.updateMapItem(item.map_id, item.id, {
+ properties: {
+ ...item.properties,
+ notes: notes,
+ image: imageData,
+ },
+ });
+ onUpdate();
+ setShowNotesDialog(false);
+ onClose();
+ } catch (error) {
+ console.error('Failed to save notes:', error);
+ alert('Failed to save notes');
+ }
+ };
+
+ const handleSavePortConfig = async () => {
+ try {
+ await mapItemService.updateMapItem(item.map_id, item.id, {
+ properties: {
+ ...item.properties,
+ port_count: portCount,
+ },
+ });
+ onUpdate();
+ setShowPortConfigDialog(false);
+ onClose();
+ } catch (error) {
+ console.error('Failed to save port configuration:', error);
+ alert('Failed to save port configuration');
+ }
+ };
+
+ if (showPortConfigDialog) {
+ return (
+
+
Configure Ports
+
+ Total number of ports:
+
+
setPortCount(parseInt(e.target.value) || 1)}
+ className="w-full px-3 py-2 border border-gray-300 rounded mb-3"
+ autoFocus
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleSavePortConfig();
+ if (e.key === 'Escape') { setShowPortConfigDialog(false); onClose(); }
+ }}
+ />
+
+ Currently used: {item.properties.connections?.length || 0} ports
+
+
+
+ Save
+
+ { setShowPortConfigDialog(false); onClose(); }}
+ className="flex-1 px-3 py-1.5 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
+ >
+ Cancel
+
+
+
+ );
+ }
+
+ if (showDeleteDialog) {
+ return (
+
+
Delete Item
+
+ Are you sure you want to delete {item.properties.name || item.type} ?
+ {item.type === 'cable' && item.properties.start_device_id && (
+ This will also remove the connection from the connected devices.
+ )}
+
+
+ {/* Checkbox for deleting connected cables */}
+ {isDevice && hasConnections && (
+
+ setDeleteConnectedCables(e.target.checked)}
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
+ />
+
+ Also delete {item.properties.connections.length} connected cable{item.properties.connections.length !== 1 ? 's' : ''}
+
+
+ )}
+
+
+
+ Delete
+
+ { setShowDeleteDialog(false); setDeleteConnectedCables(false); onClose(); }}
+ className="flex-1 px-3 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
+ >
+ Cancel
+
+
+
+ );
+ }
+
+ if (showRenameDialog) {
+ return (
+
+
Rename Item
+
setNewName(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded mb-3"
+ placeholder="Enter new name"
+ autoFocus
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleRename();
+ if (e.key === 'Escape') { setShowRenameDialog(false); onClose(); }
+ }}
+ />
+
+
+ Save
+
+ { setShowRenameDialog(false); onClose(); }}
+ className="flex-1 px-3 py-1.5 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
+ >
+ Cancel
+
+
+
+ );
+ }
+
+ if (showNotesDialog) {
+ return (
+
+ );
+ }
+
+ return (
+
+
setShowRenameDialog(true)}
+ className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100"
+ >
+ Rename
+
+
setShowNotesDialog(true)}
+ className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100"
+ >
+ {item.properties.notes ? 'Edit Notes' : 'Add Notes'}
+
+ {isSwitch && (
+
setShowPortConfigDialog(true)}
+ className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100"
+ >
+ Configure Ports
+
+ )}
+
+
setShowDeleteDialog(true)}
+ className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
+ >
+ Delete
+
+
+ );
+}
diff --git a/public/src/components/map/LayerSwitcher.tsx b/public/src/components/map/LayerSwitcher.tsx
new file mode 100644
index 0000000..b163193
--- /dev/null
+++ b/public/src/components/map/LayerSwitcher.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react';
+
+interface LayerInfo {
+ name: string;
+ url: string;
+ attribution: string;
+ maxZoom: number;
+ maxNativeZoom?: number;
+}
+
+interface LayerSwitcherProps {
+ activeLayer: string;
+ onLayerChange: (layer: string) => void;
+ layers: Record;
+}
+
+export function LayerSwitcher({ activeLayer, onLayerChange, layers }: LayerSwitcherProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
setIsOpen(!isOpen)}
+ className="bg-white shadow-md rounded-lg px-4 py-2 hover:bg-gray-50 flex items-center gap-2"
+ >
+
+
+
+
+ {layers[activeLayer]?.name || 'Map Layer'}
+
+
+
+
+
+
+ {isOpen && (
+
+ {Object.entries(layers).map(([key, layer]) => (
+
{
+ onLayerChange(key);
+ setIsOpen(false);
+ }}
+ className={`w-full px-4 py-2 text-left text-sm hover:bg-gray-50 flex items-center justify-between ${
+ activeLayer === key ? 'bg-blue-50 text-blue-700' : 'text-gray-700'
+ }`}
+ >
+ {layer.name}
+ {activeLayer === key && (
+
+
+
+ )}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/public/src/components/map/MapItemsLayer.tsx b/public/src/components/map/MapItemsLayer.tsx
new file mode 100644
index 0000000..ec294ee
--- /dev/null
+++ b/public/src/components/map/MapItemsLayer.tsx
@@ -0,0 +1,422 @@
+import { useEffect, useState } from 'react';
+import { createPortal } from 'react-dom';
+import { Polyline, Marker, Popup, Circle, useMapEvents } from 'react-leaflet';
+import L from 'leaflet';
+import { mapItemService } from '../../services/mapItemService';
+import { CABLE_COLORS, type MapItem, type CableType } from '../../types/mapItem';
+import { ItemContextMenu } from './ItemContextMenu';
+import { useDrawingStore } from '../../stores/drawingStore';
+
+interface MapItemsLayerProps {
+ mapId: string;
+ refreshTrigger: number;
+}
+
+// Custom marker icons for devices using CSS
+const switchIcon = new L.DivIcon({
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
`,
+ className: 'custom-device-marker',
+ iconSize: [40, 40],
+ iconAnchor: [20, 40],
+});
+
+const indoorApIcon = new L.DivIcon({
+ html: ``,
+ className: 'custom-device-marker',
+ iconSize: [40, 40],
+ iconAnchor: [20, 40],
+});
+
+const outdoorApIcon = new L.DivIcon({
+ html: ``,
+ className: 'custom-device-marker',
+ iconSize: [40, 40],
+ iconAnchor: [20, 40],
+});
+
+export function MapItemsLayer({ mapId, refreshTrigger }: MapItemsLayerProps) {
+ const [items, setItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [contextMenu, setContextMenu] = useState<{
+ item: MapItem;
+ position: { x: number; y: number };
+ } | null>(null);
+ const { activeTool } = useDrawingStore();
+
+ // Check if we're in drawing mode (should suppress popups)
+ const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool);
+ const isWirelessTool = activeTool === 'wireless_mesh';
+ const shouldSuppressPopups = isCableTool || isWirelessTool;
+
+ useEffect(() => {
+ loadItems();
+ }, [mapId, refreshTrigger]);
+
+ const loadItems = async () => {
+ try {
+ setLoading(true);
+ const data = await mapItemService.getMapItems(mapId);
+ console.log('Loaded items:', data);
+ // Log devices with their connections
+ data.forEach(item => {
+ if (['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type)) {
+ console.log(`Device ${item.type} (${item.id}): ${item.properties.connections?.length || 0} / ${item.properties.port_count} ports`);
+ }
+ });
+ setItems(data);
+ } catch (error) {
+ console.error('Failed to load map items:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Close context menu on any map click
+ useMapEvents({
+ click: () => setContextMenu(null),
+ contextmenu: () => {}, // Prevent default map context menu
+ });
+
+ const getDeviceIcon = (type: string) => {
+ switch (type) {
+ case 'switch':
+ return switchIcon;
+ case 'indoor_ap':
+ return indoorApIcon;
+ case 'outdoor_ap':
+ return outdoorApIcon;
+ default:
+ return undefined;
+ }
+ };
+
+ if (loading) return null;
+
+ return (
+ <>
+ {items.map((item) => {
+ // Render cables
+ if (item.type === 'cable' && item.geometry.type === 'LineString') {
+ const positions = item.geometry.coordinates.map(
+ ([lng, lat]) => [lat, lng] as [number, number]
+ );
+ const cableType = item.properties.cable_type as CableType;
+ const color = CABLE_COLORS[cableType] || '#6B7280';
+
+ // Find connected devices
+ const startDevice = item.properties.start_device_id
+ ? items.find(i => i.id === item.properties.start_device_id)
+ : null;
+ const endDevice = item.properties.end_device_id
+ ? items.find(i => i.id === item.properties.end_device_id)
+ : null;
+
+ return (
+
+
{
+ L.DomEvent.stopPropagation(e);
+ setContextMenu({
+ item,
+ position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
+ });
+ },
+ }}
+ >
+ {!shouldSuppressPopups && (
+
+
+
{item.properties.name || 'Cable'}
+
Type: {cableType}
+ {startDevice && (
+
+ From: {startDevice.properties.name || startDevice.type}
+
+ )}
+ {endDevice && (
+
+ To: {endDevice.properties.name || endDevice.type}
+
+ )}
+ {item.properties.notes && (
+
+ {item.properties.notes}
+
+ )}
+ {item.properties.image && (
+
+
+
+ )}
+
+
+ )}
+
+
+ {/* Show circles at cable bend points (not first/last) */}
+ {positions.slice(1, -1).map((pos, idx) => (
+
+ ))}
+
+ );
+ }
+
+ // Render wireless mesh
+ if (item.type === 'wireless_mesh' && item.geometry.type === 'LineString') {
+ const positions = item.geometry.coordinates.map(
+ ([lng, lat]) => [lat, lng] as [number, number]
+ );
+
+ // Find connected APs
+ const startAp = item.properties.start_ap_id
+ ? items.find(i => i.id === item.properties.start_ap_id)
+ : null;
+ const endAp = item.properties.end_ap_id
+ ? items.find(i => i.id === item.properties.end_ap_id)
+ : null;
+
+ return (
+ {
+ L.DomEvent.stopPropagation(e);
+ setContextMenu({
+ item,
+ position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
+ });
+ },
+ }}
+ >
+ {!shouldSuppressPopups && (
+
+
+
{item.properties.name || 'Wireless Mesh'}
+ {startAp && (
+
+ From: {startAp.properties.name || startAp.type}
+
+ )}
+ {endAp && (
+
+ To: {endAp.properties.name || endAp.type}
+
+ )}
+ {item.properties.notes && (
+
+ {item.properties.notes}
+
+ )}
+ {item.properties.image && (
+
+
+
+ )}
+
+
+ )}
+
+ );
+ }
+
+ // Render devices
+ if (
+ ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) &&
+ item.geometry.type === 'Point'
+ ) {
+ const [lng, lat] = item.geometry.coordinates;
+ const position: [number, number] = [lat, lng];
+ const icon = getDeviceIcon(item.type);
+
+ return (
+ {
+ L.DomEvent.stopPropagation(e);
+ setContextMenu({
+ item,
+ position: { x: e.originalEvent.clientX, y: e.originalEvent.clientY }
+ });
+ },
+ }}
+ >
+ {!shouldSuppressPopups && (
+
+
+
{item.properties.name || item.type}
+
Type: {item.type}
+ {item.properties.port_count && (
+
+ Ports: {item.properties.connections?.length || 0} / {item.properties.port_count}
+
+ )}
+
+ {/* Show port connections details */}
+ {item.properties.connections && item.properties.connections.length > 0 && (
+
+
Port Connections:
+ {item.properties.connections
+ .sort((a: any, b: any) => a.port_number - b.port_number)
+ .map((conn: any) => {
+ const cable = items.find(i => i.id === conn.cable_id);
+ if (!cable) return null;
+
+ // Find the other device connected to this cable
+ const otherDeviceId = cable.properties.start_device_id === item.id
+ ? cable.properties.end_device_id
+ : cable.properties.start_device_id;
+
+ const otherDevice = otherDeviceId
+ ? items.find(i => i.id === otherDeviceId)
+ : null;
+
+ const cableType = cable.properties.cable_type;
+
+ return (
+
+ Port {conn.port_number} → {otherDevice
+ ? `${otherDevice.properties.name || otherDevice.type} (${cableType})`
+ : `${cableType} cable`}
+
+ );
+ })}
+
+ )}
+
+ {/* Show wireless mesh count for APs */}
+ {['indoor_ap', 'outdoor_ap'].includes(item.type) && (
+
+ Wireless mesh: {items.filter(i =>
+ i.type === 'wireless_mesh' &&
+ (i.properties.start_ap_id === item.id || i.properties.end_ap_id === item.id)
+ ).length} / 4
+
+ )}
+
+ {/* Show wireless mesh connections details for APs */}
+ {['indoor_ap', 'outdoor_ap'].includes(item.type) && (() => {
+ const meshLinks = items.filter(i =>
+ i.type === 'wireless_mesh' &&
+ (i.properties.start_ap_id === item.id || i.properties.end_ap_id === item.id)
+ );
+
+ if (meshLinks.length > 0) {
+ return (
+
+
Mesh Connections:
+ {meshLinks.map((mesh) => {
+ const otherApId = mesh.properties.start_ap_id === item.id
+ ? mesh.properties.end_ap_id
+ : mesh.properties.start_ap_id;
+
+ const otherAp = otherApId
+ ? items.find(i => i.id === otherApId)
+ : null;
+
+ return (
+
+ → {otherAp ? (otherAp.properties.name || otherAp.type) : 'Unknown AP'}
+
+ );
+ })}
+
+ );
+ }
+ return null;
+ })()}
+
+ {item.properties.notes && (
+
+ {item.properties.notes}
+
+ )}
+ {item.properties.image && (
+
+
+
+ )}
+
+
+ )}
+
+ );
+ }
+
+ return null;
+ })}
+
+ {/* Context menu rendered outside map using portal */}
+ {contextMenu && createPortal(
+ setContextMenu(null)}
+ onUpdate={loadItems}
+ />,
+ document.body
+ )}
+ >
+ );
+}
diff --git a/public/src/components/map/MapListSidebar.tsx b/public/src/components/map/MapListSidebar.tsx
new file mode 100644
index 0000000..d5c641d
--- /dev/null
+++ b/public/src/components/map/MapListSidebar.tsx
@@ -0,0 +1,142 @@
+import { useState, useEffect } from 'react';
+import { useMapStore } from '../../stores/mapStore';
+import { mapService } from '../../services/mapService';
+
+interface MapListSidebarProps {
+ onSelectMap: (mapId: string) => void;
+ selectedMapId: string | null;
+}
+
+export function MapListSidebar({ onSelectMap, selectedMapId }: MapListSidebarProps) {
+ const { maps, setMaps, addMap, removeMap, setLoading, setError } = useMapStore();
+ const [isCreating, setIsCreating] = useState(false);
+ const [newMapName, setNewMapName] = useState('');
+
+ useEffect(() => {
+ loadMaps();
+ }, []);
+
+ const loadMaps = async () => {
+ setLoading(true);
+ try {
+ const data = await mapService.getUserMaps();
+ setMaps(data);
+ } catch (error: any) {
+ setError(error.response?.data?.detail || 'Failed to load maps');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreateMap = async () => {
+ if (!newMapName.trim()) return;
+
+ try {
+ const newMap = await mapService.createMap({ name: newMapName });
+ addMap(newMap);
+ setNewMapName('');
+ setIsCreating(false);
+ onSelectMap(newMap.id);
+ } catch (error: any) {
+ setError(error.response?.data?.detail || 'Failed to create map');
+ }
+ };
+
+ const handleDeleteMap = async (mapId: string) => {
+ if (!confirm('Are you sure you want to delete this map?')) return;
+
+ try {
+ await mapService.deleteMap(mapId);
+ removeMap(mapId);
+ } catch (error: any) {
+ setError(error.response?.data?.detail || 'Failed to delete map');
+ }
+ };
+
+ return (
+
+
+
My Maps
+
+ {!isCreating ? (
+
setIsCreating(true)}
+ className="w-full px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
+ >
+ + New Map
+
+ ) : (
+
+
setNewMapName(e.target.value)}
+ placeholder="Map name"
+ className="w-full px-3 py-2 border border-gray-300 rounded"
+ autoFocus
+ onKeyDown={(e) => e.key === 'Enter' && handleCreateMap()}
+ />
+
+
+ Create
+
+ {
+ setIsCreating(false);
+ setNewMapName('');
+ }}
+ className="flex-1 px-3 py-1 bg-gray-300 text-gray-700 rounded hover:bg-gray-400 text-sm"
+ >
+ Cancel
+
+
+
+ )}
+
+
+
+ {maps.length === 0 ? (
+
+ No maps yet. Create your first map!
+
+ ) : (
+
+ {maps.map((map) => (
+
onSelectMap(map.id)}
+ >
+
+
+
{map.name}
+ {map.description && (
+
{map.description}
+ )}
+
+ {new Date(map.updated_at).toLocaleDateString()}
+
+
+
{
+ e.stopPropagation();
+ handleDeleteMap(map.id);
+ }}
+ className="text-red-600 hover:text-red-800 text-sm ml-2"
+ >
+ Delete
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/public/src/components/map/MapView.tsx b/public/src/components/map/MapView.tsx
new file mode 100644
index 0000000..7eb6c6f
--- /dev/null
+++ b/public/src/components/map/MapView.tsx
@@ -0,0 +1,113 @@
+import { useState, useEffect } from 'react';
+import { MapContainer, TileLayer, useMap } from 'react-leaflet';
+import 'leaflet/dist/leaflet.css';
+import { Toolbar } from './Toolbar';
+import { LayerSwitcher } from './LayerSwitcher';
+import { DrawingHandler } from './DrawingHandler';
+import { MapItemsLayer } from './MapItemsLayer';
+
+interface MapViewProps {
+ mapId: string | null;
+}
+
+type MapLayer = 'osm' | 'google' | 'esri';
+
+const MAP_LAYERS = {
+ osm: {
+ name: 'OpenStreetMap',
+ url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ attribution: '© OpenStreetMap contributors',
+ maxZoom: 25,
+ },
+ google: {
+ name: 'Google Satellite',
+ url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
+ attribution: '© Google',
+ maxZoom: 25,
+ maxNativeZoom: 22,
+ },
+ esri: {
+ name: 'ESRI Satellite',
+ url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
+ attribution: 'Tiles © Esri',
+ maxZoom: 25,
+ },
+};
+
+function MapController() {
+ const map = useMap();
+
+ useEffect(() => {
+ // Invalidate size when map becomes visible
+ setTimeout(() => {
+ map.invalidateSize();
+ }, 100);
+ }, [map]);
+
+ return null;
+}
+
+export function MapView({ mapId }: MapViewProps) {
+ const [activeLayer, setActiveLayer] = useState('osm');
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+
+ const handleItemCreated = () => {
+ // Trigger refresh of map items
+ setRefreshTrigger((prev) => prev + 1);
+ };
+
+ if (!mapId) {
+ return (
+
+
+
Select a map from the sidebar to get started
+
or create a new one
+
+
+ );
+ }
+
+ const layer = MAP_LAYERS[activeLayer];
+
+ return (
+ <>
+ {/* Toolbar for drawing tools */}
+
+
+
+
+ {/* Layer switcher */}
+
+
+
+
+
+
+
+
+
+ {/* Drawing handler for creating new items */}
+
+
+ {/* Render existing map items */}
+
+
+
+ >
+ );
+}
diff --git a/public/src/components/map/Toolbar.tsx b/public/src/components/map/Toolbar.tsx
new file mode 100644
index 0000000..cb7f3f3
--- /dev/null
+++ b/public/src/components/map/Toolbar.tsx
@@ -0,0 +1,99 @@
+import { useDrawingStore } from '../../stores/drawingStore';
+import type { DrawingTool } from '../../types/mapItem';
+import { CABLE_COLORS, CABLE_LABELS } from '../../types/mapItem';
+
+interface ToolbarProps {
+ mapId: string;
+}
+
+interface ToolButton {
+ id: DrawingTool;
+ label: string;
+ icon: string;
+ color?: string;
+ description: string;
+}
+
+const TOOLS: ToolButton[] = [
+ {
+ id: 'select',
+ label: 'Select',
+ icon: '👆',
+ description: 'Select and edit items',
+ },
+ {
+ id: 'fiber',
+ label: 'Fiber',
+ icon: '━',
+ color: CABLE_COLORS.fiber,
+ description: CABLE_LABELS.fiber,
+ },
+ {
+ id: 'cat6',
+ label: 'Cat6',
+ icon: '━',
+ color: CABLE_COLORS.cat6,
+ description: CABLE_LABELS.cat6,
+ },
+ {
+ id: 'cat6_poe',
+ label: 'Cat6 PoE',
+ icon: '━',
+ color: CABLE_COLORS.cat6_poe,
+ description: CABLE_LABELS.cat6_poe,
+ },
+ {
+ id: 'switch',
+ label: 'Switch',
+ icon: '⚡',
+ description: 'Network Switch',
+ },
+ {
+ id: 'indoor_ap',
+ label: 'Indoor AP',
+ icon: '📡',
+ description: 'Indoor Access Point',
+ },
+ {
+ id: 'outdoor_ap',
+ label: 'Outdoor AP',
+ icon: '📶',
+ description: 'Outdoor Access Point',
+ },
+ {
+ id: 'wireless_mesh',
+ label: 'Wireless',
+ icon: '⚡',
+ color: '#10B981',
+ description: 'Wireless Mesh Link',
+ },
+];
+
+export function Toolbar({ mapId }: ToolbarProps) {
+ const { activeTool, setActiveTool } = useDrawingStore();
+
+ return (
+
+ {TOOLS.map((tool) => (
+ setActiveTool(tool.id)}
+ className={`w-full px-3 py-2 rounded text-left flex items-center gap-2 transition-colors ${
+ activeTool === tool.id
+ ? 'bg-blue-100 text-blue-700 font-medium'
+ : 'hover:bg-gray-100 text-gray-700'
+ }`}
+ title={tool.description}
+ >
+
+ {tool.icon}
+
+ {tool.label}
+
+ ))}
+
+ );
+}
diff --git a/public/src/index.css b/public/src/index.css
new file mode 100644
index 0000000..cbed6c0
--- /dev/null
+++ b/public/src/index.css
@@ -0,0 +1,56 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Leaflet CSS will be imported in main.tsx */
+
+/* Custom Leaflet marker styles */
+.custom-marker {
+ background: transparent !important;
+ border: none !important;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.custom-marker div {
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
+}
+
+/* Device marker icons */
+.custom-device-marker {
+ background: transparent !important;
+ border: none !important;
+}
+
+.device-icon {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: white;
+ border-radius: 50%;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+ border: 3px solid;
+}
+
+.device-icon svg {
+ width: 24px;
+ height: 24px;
+}
+
+.switch-icon {
+ border-color: #8B5CF6;
+ color: #8B5CF6;
+}
+
+.ap-indoor-icon {
+ border-color: #10B981;
+ color: #10B981;
+}
+
+.ap-outdoor-icon {
+ border-color: #F59E0B;
+ color: #F59E0B;
+}
diff --git a/public/src/main.tsx b/public/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/public/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/public/src/pages/Dashboard.tsx b/public/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..bc82909
--- /dev/null
+++ b/public/src/pages/Dashboard.tsx
@@ -0,0 +1,20 @@
+import { useState } from 'react';
+import { Layout } from '../components/layout/Layout';
+import { MapListSidebar } from '../components/map/MapListSidebar';
+import { MapView } from '../components/map/MapView';
+
+export function Dashboard() {
+ const [selectedMapId, setSelectedMapId] = useState(null);
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/public/src/services/api.ts b/public/src/services/api.ts
new file mode 100644
index 0000000..a57cb99
--- /dev/null
+++ b/public/src/services/api.ts
@@ -0,0 +1,38 @@
+import axios from 'axios';
+
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
+
+export const apiClient = axios.create({
+ baseURL: API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor to add auth token
+apiClient.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('access_token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor to handle 401 errors
+apiClient.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401) {
+ // Clear tokens and redirect to login
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('refresh_token');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ }
+);
diff --git a/public/src/services/authService.ts b/public/src/services/authService.ts
new file mode 100644
index 0000000..d6293d6
--- /dev/null
+++ b/public/src/services/authService.ts
@@ -0,0 +1,32 @@
+import { apiClient } from './api';
+import type { LoginRequest, TokenResponse, User } from '../types/auth';
+
+export const authService = {
+ async login(credentials: LoginRequest): Promise {
+ const response = await apiClient.post('/api/auth/login', credentials);
+ return response.data;
+ },
+
+ async getCurrentUser(): Promise {
+ const response = await apiClient.get('/api/auth/me');
+ return response.data;
+ },
+
+ logout() {
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('refresh_token');
+ },
+
+ saveTokens(tokens: TokenResponse) {
+ localStorage.setItem('access_token', tokens.access_token);
+ localStorage.setItem('refresh_token', tokens.refresh_token);
+ },
+
+ getAccessToken(): string | null {
+ return localStorage.getItem('access_token');
+ },
+
+ isAuthenticated(): boolean {
+ return !!this.getAccessToken();
+ },
+};
diff --git a/public/src/services/mapItemService.ts b/public/src/services/mapItemService.ts
new file mode 100644
index 0000000..456a97a
--- /dev/null
+++ b/public/src/services/mapItemService.ts
@@ -0,0 +1,28 @@
+import { apiClient } from './api';
+import type { MapItem, MapItemCreate, MapItemUpdate } from '../types/mapItem';
+
+export const mapItemService = {
+ async getMapItems(mapId: string): Promise {
+ const response = await apiClient.get(`/api/maps/${mapId}/items`);
+ return response.data;
+ },
+
+ async getMapItem(mapId: string, itemId: string): Promise {
+ const response = await apiClient.get(`/api/maps/${mapId}/items/${itemId}`);
+ return response.data;
+ },
+
+ async createMapItem(mapId: string, data: MapItemCreate): Promise {
+ const response = await apiClient.post(`/api/maps/${mapId}/items`, data);
+ return response.data;
+ },
+
+ async updateMapItem(mapId: string, itemId: string, data: MapItemUpdate): Promise {
+ const response = await apiClient.patch(`/api/maps/${mapId}/items/${itemId}`, data);
+ return response.data;
+ },
+
+ async deleteMapItem(mapId: string, itemId: string): Promise {
+ await apiClient.delete(`/api/maps/${mapId}/items/${itemId}`);
+ },
+};
diff --git a/public/src/services/mapService.ts b/public/src/services/mapService.ts
new file mode 100644
index 0000000..94e7254
--- /dev/null
+++ b/public/src/services/mapService.ts
@@ -0,0 +1,38 @@
+import { apiClient } from './api';
+import type { Map, MapCreate, MapUpdate } from '../types/map';
+
+export const mapService = {
+ async getUserMaps(): Promise {
+ const response = await apiClient.get('/api/maps');
+ return response.data;
+ },
+
+ async getMap(mapId: string): Promise {
+ const response = await apiClient.get(`/api/maps/${mapId}`);
+ return response.data;
+ },
+
+ async getPublicMap(): Promise {
+ const response = await apiClient.get('/api/maps/public');
+ return response.data;
+ },
+
+ async createMap(data: MapCreate): Promise {
+ const response = await apiClient.post('/api/maps', data);
+ return response.data;
+ },
+
+ async updateMap(mapId: string, data: MapUpdate): Promise {
+ const response = await apiClient.patch(`/api/maps/${mapId}`, data);
+ return response.data;
+ },
+
+ async deleteMap(mapId: string): Promise {
+ await apiClient.delete(`/api/maps/${mapId}`);
+ },
+
+ async setDefaultPublic(mapId: string): Promise {
+ const response = await apiClient.post(`/api/maps/${mapId}/set-default-public`);
+ return response.data;
+ },
+};
diff --git a/public/src/stores/authStore.ts b/public/src/stores/authStore.ts
new file mode 100644
index 0000000..8c9ca36
--- /dev/null
+++ b/public/src/stores/authStore.ts
@@ -0,0 +1,60 @@
+import { create } from 'zustand';
+import type { User } from '../types/auth';
+import { authService } from '../services/authService';
+
+interface AuthState {
+ user: User | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ error: string | null;
+
+ login: (username: string, password: string) => Promise;
+ logout: () => void;
+ loadUser: () => Promise;
+ clearError: () => void;
+}
+
+export const useAuthStore = create((set) => ({
+ user: null,
+ isAuthenticated: authService.isAuthenticated(),
+ isLoading: false,
+ error: null,
+
+ login: async (username, password) => {
+ set({ isLoading: true, error: null });
+ try {
+ const tokens = await authService.login({ username, password });
+ authService.saveTokens(tokens);
+
+ const user = await authService.getCurrentUser();
+ set({ user, isAuthenticated: true, isLoading: false });
+ } catch (error: any) {
+ const message = error.response?.data?.detail || 'Login failed';
+ set({ error: message, isLoading: false, isAuthenticated: false });
+ throw error;
+ }
+ },
+
+ logout: () => {
+ authService.logout();
+ set({ user: null, isAuthenticated: false });
+ },
+
+ loadUser: async () => {
+ if (!authService.isAuthenticated()) {
+ set({ isAuthenticated: false, user: null });
+ return;
+ }
+
+ set({ isLoading: true });
+ try {
+ const user = await authService.getCurrentUser();
+ set({ user, isAuthenticated: true, isLoading: false });
+ } catch (error) {
+ authService.logout();
+ set({ user: null, isAuthenticated: false, isLoading: false });
+ }
+ },
+
+ clearError: () => set({ error: null }),
+}));
diff --git a/public/src/stores/drawingStore.ts b/public/src/stores/drawingStore.ts
new file mode 100644
index 0000000..cf2c75e
--- /dev/null
+++ b/public/src/stores/drawingStore.ts
@@ -0,0 +1,35 @@
+import { create } from 'zustand';
+import type { DrawingTool } from '../types/mapItem';
+
+interface DrawingState {
+ activeTool: DrawingTool;
+ isDrawing: boolean;
+ drawingPoints: [number, number][];
+
+ setActiveTool: (tool: DrawingTool) => void;
+ setIsDrawing: (isDrawing: boolean) => void;
+ addDrawingPoint: (point: [number, number]) => void;
+ clearDrawingPoints: () => void;
+ resetDrawing: () => void;
+}
+
+export const useDrawingStore = create((set) => ({
+ activeTool: 'select',
+ isDrawing: false,
+ drawingPoints: [],
+
+ setActiveTool: (tool) => set({ activeTool: tool, isDrawing: false, drawingPoints: [] }),
+
+ setIsDrawing: (isDrawing) => set({ isDrawing }),
+
+ addDrawingPoint: (point) => set((state) => ({
+ drawingPoints: [...state.drawingPoints, point],
+ })),
+
+ clearDrawingPoints: () => set({ drawingPoints: [] }),
+
+ resetDrawing: () => set({
+ isDrawing: false,
+ drawingPoints: [],
+ }),
+}));
diff --git a/public/src/stores/mapStore.ts b/public/src/stores/mapStore.ts
new file mode 100644
index 0000000..c8c0eea
--- /dev/null
+++ b/public/src/stores/mapStore.ts
@@ -0,0 +1,49 @@
+import { create } from 'zustand';
+import type { Map } from '../types/map';
+
+interface MapState {
+ maps: Map[];
+ currentMap: Map | null;
+ isLoading: boolean;
+ error: string | null;
+
+ setMaps: (maps: Map[]) => void;
+ setCurrentMap: (map: Map | null) => void;
+ addMap: (map: Map) => void;
+ updateMap: (mapId: string, updates: Partial) => void;
+ removeMap: (mapId: string) => void;
+ setLoading: (isLoading: boolean) => void;
+ setError: (error: string | null) => void;
+ clearError: () => void;
+}
+
+export const useMapStore = create((set) => ({
+ maps: [],
+ currentMap: null,
+ isLoading: false,
+ error: null,
+
+ setMaps: (maps) => set({ maps }),
+
+ setCurrentMap: (map) => set({ currentMap: map }),
+
+ addMap: (map) => set((state) => ({ maps: [map, ...state.maps] })),
+
+ updateMap: (mapId, updates) => set((state) => ({
+ maps: state.maps.map((m) => (m.id === mapId ? { ...m, ...updates } : m)),
+ currentMap: state.currentMap?.id === mapId
+ ? { ...state.currentMap, ...updates }
+ : state.currentMap,
+ })),
+
+ removeMap: (mapId) => set((state) => ({
+ maps: state.maps.filter((m) => m.id !== mapId),
+ currentMap: state.currentMap?.id === mapId ? null : state.currentMap,
+ })),
+
+ setLoading: (isLoading) => set({ isLoading }),
+
+ setError: (error) => set({ error }),
+
+ clearError: () => set({ error: null }),
+}));
diff --git a/public/src/types/auth.ts b/public/src/types/auth.ts
new file mode 100644
index 0000000..4535f4d
--- /dev/null
+++ b/public/src/types/auth.ts
@@ -0,0 +1,25 @@
+export interface User {
+ id: string;
+ username: string;
+ email: string;
+ is_admin: boolean;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface LoginRequest {
+ username: string;
+ password: string;
+}
+
+export interface TokenResponse {
+ access_token: string;
+ refresh_token: string;
+ token_type: string;
+}
+
+export interface UserWithToken extends User {
+ access_token: string;
+ refresh_token: string;
+ token_type: string;
+}
diff --git a/public/src/types/map.ts b/public/src/types/map.ts
new file mode 100644
index 0000000..16e9f16
--- /dev/null
+++ b/public/src/types/map.ts
@@ -0,0 +1,19 @@
+export interface Map {
+ id: string;
+ name: string;
+ description: string | null;
+ owner_id: string;
+ is_default_public: boolean;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface MapCreate {
+ name: string;
+ description?: string | null;
+}
+
+export interface MapUpdate {
+ name?: string;
+ description?: string | null;
+}
diff --git a/public/src/types/mapItem.ts b/public/src/types/mapItem.ts
new file mode 100644
index 0000000..1abd3bb
--- /dev/null
+++ b/public/src/types/mapItem.ts
@@ -0,0 +1,75 @@
+export type MapItemType = 'cable' | 'switch' | 'indoor_ap' | 'outdoor_ap' | 'wireless_mesh';
+
+export type CableType = 'fiber' | 'cat6' | 'cat6_poe';
+
+export interface MapItem {
+ id: string;
+ map_id: string;
+ type: MapItemType;
+ geometry: GeoJSON.Geometry;
+ properties: Record;
+ created_at: string;
+ updated_at: string;
+ created_by: string | null;
+ updated_by: string | null;
+}
+
+export interface CableProperties {
+ cable_type: CableType;
+ name?: string;
+ notes?: string;
+ length_meters?: number;
+ start_device_id?: string | null;
+ end_device_id?: string | null;
+}
+
+export interface DeviceProperties {
+ name?: string;
+ notes?: string;
+ port_count?: number;
+ connections?: Array<{
+ cable_id: string;
+ port_number: number;
+ }>;
+}
+
+export interface WirelessMeshProperties {
+ name?: string;
+ notes?: string;
+ start_ap_id?: string;
+ end_ap_id?: string;
+}
+
+export interface MapItemCreate {
+ type: MapItemType;
+ geometry: GeoJSON.Geometry;
+ properties: Record;
+}
+
+export interface MapItemUpdate {
+ type?: MapItemType;
+ geometry?: GeoJSON.Geometry;
+ properties?: Record;
+}
+
+export type DrawingTool =
+ | 'select'
+ | 'fiber'
+ | 'cat6'
+ | 'cat6_poe'
+ | 'switch'
+ | 'indoor_ap'
+ | 'outdoor_ap'
+ | 'wireless_mesh';
+
+export const CABLE_COLORS: Record = {
+ fiber: '#3B82F6', // Blue
+ cat6: '#F97316', // Orange
+ cat6_poe: '#EF4444', // Red
+};
+
+export const CABLE_LABELS: Record = {
+ fiber: 'Fiber Cable',
+ cat6: 'Cat6 Cable',
+ cat6_poe: 'Cat6 PoE Cable',
+};
diff --git a/public/tailwind.config.js b/public/tailwind.config.js
new file mode 100644
index 0000000..dca8ba0
--- /dev/null
+++ b/public/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/public/tsconfig.app.json b/public/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/public/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/public/tsconfig.json b/public/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/public/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/public/tsconfig.node.json b/public/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/public/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/public/vite.config.ts b/public/vite.config.ts
new file mode 100644
index 0000000..c8c6bc9
--- /dev/null
+++ b/public/vite.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ host: '0.0.0.0',
+ },
+})
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..093ca23
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,35 @@
+# FastAPI and ASGI server
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+python-multipart==0.0.6
+
+# Database
+sqlalchemy==2.0.23
+alembic==1.12.1
+psycopg2-binary==2.9.9
+geoalchemy2==0.14.2
+numpy==1.26.4
+shapely==2.0.2
+
+# Pydantic for validation
+pydantic[email]==2.5.0
+pydantic-settings==2.1.0
+
+# Authentication
+python-jose[cryptography]==3.3.0
+passlib==1.7.4
+bcrypt==4.0.1
+
+# WebSocket for real-time collaboration
+python-socketio==5.10.0
+
+# CORS middleware
+python-dotenv==1.0.0
+
+# Testing
+pytest==7.4.3
+pytest-asyncio==0.21.1
+httpx==0.25.1
+
+# Production server
+gunicorn==21.2.0
diff --git a/scripts/create_admin.py b/scripts/create_admin.py
new file mode 100644
index 0000000..206cc7e
--- /dev/null
+++ b/scripts/create_admin.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""
+Script to create an admin user for the ISP Wiremap application.
+Usage: python scripts/create_admin.py
+"""
+import sys
+from pathlib import Path
+
+# Add parent directory to path to import app modules
+sys.path.append(str(Path(__file__).resolve().parents[1]))
+
+from app.database import SessionLocal
+from app.models.user import User
+from app.utils.password import hash_password
+import getpass
+
+
+def create_admin_user():
+ """Create an admin user interactively."""
+ print("=" * 50)
+ print("ISP Wiremap - Create Admin User")
+ print("=" * 50)
+ print()
+
+ # Get user input
+ username = input("Enter admin username: ").strip()
+ if not username:
+ print("Error: Username cannot be empty")
+ return
+
+ email = input("Enter admin email: ").strip()
+ if not email:
+ print("Error: Email cannot be empty")
+ return
+
+ password = getpass.getpass("Enter admin password: ")
+ if not password:
+ print("Error: Password cannot be empty")
+ return
+
+ password_confirm = getpass.getpass("Confirm password: ")
+ if password != password_confirm:
+ print("Error: Passwords do not match")
+ return
+
+ # Create database session
+ db = SessionLocal()
+
+ try:
+ # Check if username already exists
+ existing_user = db.query(User).filter(User.username == username).first()
+ if existing_user:
+ print(f"Error: Username '{username}' already exists")
+ return
+
+ # Check if email already exists
+ existing_email = db.query(User).filter(User.email == email).first()
+ if existing_email:
+ print(f"Error: Email '{email}' already exists")
+ return
+
+ # Create admin user
+ hashed_password = hash_password(password)
+ admin_user = User(
+ username=username,
+ email=email,
+ password_hash=hashed_password,
+ is_admin=True
+ )
+
+ db.add(admin_user)
+ db.commit()
+ db.refresh(admin_user)
+
+ print()
+ print("=" * 50)
+ print("Admin user created successfully!")
+ print(f"Username: {admin_user.username}")
+ print(f"Email: {admin_user.email}")
+ print(f"User ID: {admin_user.id}")
+ print(f"Is Admin: {admin_user.is_admin}")
+ print("=" * 50)
+
+ except Exception as e:
+ db.rollback()
+ print(f"Error creating admin user: {e}")
+
+ finally:
+ db.close()
+
+
+if __name__ == "__main__":
+ create_admin_user()