From 9d886076d60165108c3e6fd8a724efdd708e9687 Mon Sep 17 00:00:00 2001 From: xds Date: Mon, 16 Mar 2026 12:12:56 +0300 Subject: [PATCH] init --- .claude/settings.local.json | 13 + .env.example | 23 + .gitignore | 40 + backend/alembic.ini | 36 + backend/alembic/env.py | 58 + backend/alembic/script.py.mako | 26 + backend/app/__init__.py | 0 backend/app/api/__init__.py | 0 backend/app/api/activities.py | 102 + backend/app/api/auth.py | 117 + backend/app/api/rider.py | 52 + backend/app/api/router.py | 11 + backend/app/core/__init__.py | 0 backend/app/core/auth.py | 36 + backend/app/core/config.py | 34 + backend/app/core/database.py | 17 + backend/app/core/security.py | 71 + backend/app/main.py | 37 + backend/app/models/__init__.py | 16 + backend/app/models/activity.py | 84 + backend/app/models/fitness.py | 42 + backend/app/models/rider.py | 29 + backend/app/models/training.py | 23 + backend/app/schemas/__init__.py | 0 backend/app/schemas/activity.py | 52 + backend/app/schemas/auth.py | 23 + backend/app/schemas/rider.py | 38 + backend/app/services/__init__.py | 0 backend/app/services/fit_parser.py | 102 + backend/app/services/gemini_client.py | 126 ++ backend/app/services/metrics.py | 83 + backend/requirements.txt | 34 + backend/tests/__init__.py | 0 frontend/.gitignore | 24 + frontend/README.md | 5 + frontend/index.html | 14 + frontend/package-lock.json | 2490 +++++++++++++++++++++ frontend/package.json | 35 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.vue | 31 + frontend/src/assets/hero.png | Bin 0 -> 44919 bytes frontend/src/assets/vite.svg | 1 + frontend/src/assets/vue.svg | 1 + frontend/src/composables/useApi.ts | 28 + frontend/src/main.ts | 26 + frontend/src/router.ts | 52 + frontend/src/stores/activities.ts | 32 + frontend/src/stores/auth.ts | 45 + frontend/src/stores/rider.ts | 21 + frontend/src/style.css | 3 + frontend/src/types/models.ts | 50 + frontend/src/types/telegram.d.ts | 31 + frontend/src/views/ActivitiesView.vue | 18 + frontend/src/views/ActivityDetailView.vue | 39 + frontend/src/views/DashboardView.vue | 29 + frontend/src/views/LoginView.vue | 78 + frontend/src/views/SettingsView.vue | 14 + frontend/tsconfig.app.json | 16 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 16 + velobrain_architecture.docx | Bin 0 -> 22981 bytes 63 files changed, 4482 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/activities.py create mode 100644 backend/app/api/auth.py create mode 100644 backend/app/api/rider.py create mode 100644 backend/app/api/router.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/auth.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/database.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/activity.py create mode 100644 backend/app/models/fitness.py create mode 100644 backend/app/models/rider.py create mode 100644 backend/app/models/training.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/activity.py create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/app/schemas/rider.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/fit_parser.py create mode 100644 backend/app/services/gemini_client.py create mode 100644 backend/app/services/metrics.py create mode 100644 backend/requirements.txt create mode 100644 backend/tests/__init__.py create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/assets/vue.svg create mode 100644 frontend/src/composables/useApi.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router.ts create mode 100644 frontend/src/stores/activities.ts create mode 100644 frontend/src/stores/auth.ts create mode 100644 frontend/src/stores/rider.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/types/models.ts create mode 100644 frontend/src/types/telegram.d.ts create mode 100644 frontend/src/views/ActivitiesView.vue create mode 100644 frontend/src/views/ActivityDetailView.vue create mode 100644 frontend/src/views/DashboardView.vue create mode 100644 frontend/src/views/LoginView.vue create mode 100644 frontend/src/views/SettingsView.vue create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 velobrain_architecture.docx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..52d0be3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(cd:*)", + "Bash(which node:*)", + "WebSearch", + "WebFetch(domain:gearboxgo.com)", + "WebFetch(domain:primevue.org)", + "WebFetch(domain:core.telegram.org)", + "WebFetch(domain:docs.telegram-mini-apps.com)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cc38bf6 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Database +DATABASE_URL=postgresql+asyncpg://velobrain:velobrain@localhost:5432/velobrain + +# Anthropic +ANTHROPIC_API_KEY=sk-ant-... + +# Gemini +GEMINI_API_KEY= +GEMINI_MODEL=gemini-2.5-pro + +# App +APP_SECRET_KEY=change-me-in-production +DEBUG=true + +# Auth / JWT +JWT_SECRET_KEY=change-me-jwt-secret + +# Telegram Bot +TELEGRAM_BOT_TOKEN= +TELEGRAM_BOT_USERNAME= + +# Upload +UPLOAD_DIR=./uploads diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e26ac1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Environment +.env + +# OS +.DS_Store +Thumbs.db + +# Alembic +backend/alembic/versions/__pycache__/ + +# Node +frontend/node_modules/ +frontend/dist/ + +# Uploads +backend/uploads/ + +# Tests +.pytest_cache/ +.coverage +htmlcov/ diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..3424864 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,36 @@ +[alembic] +script_location = alembic +sqlalchemy.url = postgresql+asyncpg://velobrain:velobrain@localhost:5432/velobrain + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[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/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..813d275 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,58 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import create_async_engine + +from backend.app.core.config import settings +from backend.app.core.database import Base + +# Import all models so they register with Base.metadata +from backend.app.models import * # noqa: F401, F403 + +config = context.config +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + 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 do_run_migrations(connection): + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + connectable = create_async_engine( + settings.DATABASE_URL, + poolclass=pool.NullPool, + ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + await connectable.dispose() + + +def run_migrations_online() -> None: + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/activities.py b/backend/app/api/activities.py new file mode 100644 index 0000000..03eacf1 --- /dev/null +++ b/backend/app/api/activities.py @@ -0,0 +1,102 @@ +import uuid +from pathlib import Path + +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.config import settings +from backend.app.core.database import get_session +from backend.app.models.activity import Activity, DataPoint +from backend.app.schemas.activity import ( + ActivityResponse, + ActivityListResponse, + DataPointResponse, +) +from backend.app.services.fit_parser import parse_fit_file +from backend.app.services.metrics import calculate_metrics + +router = APIRouter() + + +@router.post("/upload", response_model=ActivityResponse) +async def upload_activity( + rider_id: uuid.UUID, + file: UploadFile = File(...), + session: AsyncSession = Depends(get_session), +): + if not file.filename or not file.filename.lower().endswith(".fit"): + raise HTTPException(status_code=400, detail="Only .FIT files are accepted") + + upload_dir = Path(settings.UPLOAD_DIR) + upload_dir.mkdir(parents=True, exist_ok=True) + + file_id = uuid.uuid4() + file_path = upload_dir / f"{file_id}.fit" + + content = await file.read() + file_path.write_bytes(content) + + activity, data_points = parse_fit_file(content, rider_id, str(file_path)) + session.add(activity) + await session.flush() + + for dp in data_points: + dp.activity_id = activity.id + session.add_all(data_points) + + metrics = calculate_metrics(data_points, activity, rider_id, session) + if metrics: + session.add(metrics) + + await session.commit() + await session.refresh(activity) + return activity + + +@router.get("", response_model=ActivityListResponse) +async def list_activities( + rider_id: uuid.UUID, + limit: int = 20, + offset: int = 0, + session: AsyncSession = Depends(get_session), +): + count_query = select(func.count(Activity.id)).where(Activity.rider_id == rider_id) + total = (await session.execute(count_query)).scalar() or 0 + + query = ( + select(Activity) + .where(Activity.rider_id == rider_id) + .order_by(Activity.date.desc()) + .limit(limit) + .offset(offset) + ) + result = await session.execute(query) + activities = result.scalars().all() + + return ActivityListResponse(items=activities, total=total) + + +@router.get("/{activity_id}", response_model=ActivityResponse) +async def get_activity( + activity_id: uuid.UUID, + session: AsyncSession = Depends(get_session), +): + activity = await session.get(Activity, activity_id) + if not activity: + raise HTTPException(status_code=404, detail="Activity not found") + return activity + + +@router.get("/{activity_id}/stream", response_model=list[DataPointResponse]) +async def get_activity_stream( + activity_id: uuid.UUID, + session: AsyncSession = Depends(get_session), +): + query = ( + select(DataPoint) + .where(DataPoint.activity_id == activity_id) + .order_by(DataPoint.timestamp) + ) + result = await session.execute(query) + return result.scalars().all() diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..24da55b --- /dev/null +++ b/backend/app/api/auth.py @@ -0,0 +1,117 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.config import settings +from backend.app.core.database import get_session +from backend.app.core.security import ( + create_access_token, + verify_telegram_login, + verify_telegram_webapp, +) +from backend.app.models.rider import Rider +from backend.app.schemas.auth import ( + AuthResponse, + TelegramLoginRequest, + TelegramWebAppRequest, +) + +router = APIRouter() + + +async def _upsert_rider( + session: AsyncSession, + telegram_id: int, + first_name: str, + last_name: str | None, + username: str | None, + photo_url: str | None, +) -> Rider: + result = await session.execute( + select(Rider).where(Rider.telegram_id == telegram_id) + ) + rider = result.scalar_one_or_none() + + name = first_name + if last_name: + name = f"{first_name} {last_name}" + + if not rider: + rider = Rider( + telegram_id=telegram_id, + name=name, + telegram_username=username, + avatar_url=photo_url, + ) + session.add(rider) + else: + rider.name = name + rider.telegram_username = username + rider.avatar_url = photo_url + + await session.commit() + await session.refresh(rider) + return rider + + +def _build_auth_response(rider: Rider) -> AuthResponse: + token = create_access_token( + rider_id=str(rider.id), + telegram_id=rider.telegram_id, + secret=settings.JWT_SECRET_KEY, + algorithm=settings.JWT_ALGORITHM, + expires_minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES, + ) + return AuthResponse(access_token=token, rider=rider) + + +@router.post("/telegram-login", response_model=AuthResponse) +async def telegram_login( + data: TelegramLoginRequest, + session: AsyncSession = Depends(get_session), +): + if not settings.TELEGRAM_BOT_TOKEN: + raise HTTPException(status_code=500, detail="Telegram bot token not configured") + + login_data = data.model_dump() + if not verify_telegram_login(login_data, settings.TELEGRAM_BOT_TOKEN): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid Telegram authorization", + ) + + rider = await _upsert_rider( + session, + telegram_id=data.id, + first_name=data.first_name, + last_name=data.last_name, + username=data.username, + photo_url=data.photo_url, + ) + return _build_auth_response(rider) + + +@router.post("/telegram-webapp", response_model=AuthResponse) +async def telegram_webapp( + data: TelegramWebAppRequest, + session: AsyncSession = Depends(get_session), +): + if not settings.TELEGRAM_BOT_TOKEN: + raise HTTPException(status_code=500, detail="Telegram bot token not configured") + + user = verify_telegram_webapp(data.init_data, settings.TELEGRAM_BOT_TOKEN) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid Telegram WebApp data", + ) + + rider = await _upsert_rider( + session, + telegram_id=user["id"], + first_name=user.get("first_name", ""), + last_name=user.get("last_name"), + username=user.get("username"), + photo_url=user.get("photo_url"), + ) + return _build_auth_response(rider) diff --git a/backend/app/api/rider.py b/backend/app/api/rider.py new file mode 100644 index 0000000..3d2a14a --- /dev/null +++ b/backend/app/api/rider.py @@ -0,0 +1,52 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.database import get_session +from backend.app.models.rider import Rider +from backend.app.schemas.rider import RiderCreate, RiderUpdate, RiderResponse + +router = APIRouter() + + +@router.post("/profile", response_model=RiderResponse) +async def create_rider( + data: RiderCreate, + session: AsyncSession = Depends(get_session), +): + rider = Rider(**data.model_dump()) + session.add(rider) + await session.commit() + await session.refresh(rider) + return rider + + +@router.get("/profile/{rider_id}", response_model=RiderResponse) +async def get_rider( + rider_id: uuid.UUID, + session: AsyncSession = Depends(get_session), +): + rider = await session.get(Rider, rider_id) + if not rider: + raise HTTPException(status_code=404, detail="Rider not found") + return rider + + +@router.put("/profile/{rider_id}", response_model=RiderResponse) +async def update_rider( + rider_id: uuid.UUID, + data: RiderUpdate, + session: AsyncSession = Depends(get_session), +): + rider = await session.get(Rider, rider_id) + if not rider: + raise HTTPException(status_code=404, detail="Rider not found") + + update_data = data.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(rider, key, value) + + await session.commit() + await session.refresh(rider) + return rider diff --git a/backend/app/api/router.py b/backend/app/api/router.py new file mode 100644 index 0000000..7a83694 --- /dev/null +++ b/backend/app/api/router.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +from backend.app.api.auth import router as auth_router +from backend.app.api.activities import router as activities_router +from backend.app.api.rider import router as rider_router + +api_router = APIRouter(prefix="/api") + +api_router.include_router(auth_router, prefix="/auth", tags=["auth"]) +api_router.include_router(activities_router, prefix="/activities", tags=["activities"]) +api_router.include_router(rider_router, prefix="/rider", tags=["rider"]) diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py new file mode 100644 index 0000000..eaf7160 --- /dev/null +++ b/backend/app/core/auth.py @@ -0,0 +1,36 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.config import settings +from backend.app.core.database import get_session +from backend.app.core.security import decode_access_token +from backend.app.models.rider import Rider + +bearer_scheme = HTTPBearer() + + +async def get_current_rider( + credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme), + session: AsyncSession = Depends(get_session), +) -> Rider: + try: + payload = decode_access_token( + credentials.credentials, + settings.JWT_SECRET_KEY, + settings.JWT_ALGORITHM, + ) + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired token", + ) + + rider = await session.get(Rider, payload["sub"]) + if not rider: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Rider not found", + ) + + return rider diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..5f3ad96 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,34 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + model_config = {"env_file": ".env", "env_file_encoding": "utf-8"} + + # Database + DATABASE_URL: str = "postgresql+asyncpg://velobrain:velobrain@localhost:5432/velobrain" + + # Anthropic + ANTHROPIC_API_KEY: str = "" + + # Gemini + GEMINI_API_KEY: str = "" + GEMINI_MODEL: str = "gemini-2.5-pro" + + # App + APP_SECRET_KEY: str = "change-me-in-production" + DEBUG: bool = True + + # Auth / JWT + JWT_SECRET_KEY: str = "change-me-jwt-secret" + JWT_ALGORITHM: str = "HS256" + JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24 hours + + # Telegram + TELEGRAM_BOT_TOKEN: str = "" + TELEGRAM_BOT_USERNAME: str = "" + + # Upload + UPLOAD_DIR: str = "./uploads" + + +settings = Settings() diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..6419f60 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession +from sqlalchemy.orm import DeclarativeBase + +from backend.app.core.config import settings + +engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG) + +async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +class Base(DeclarativeBase): + pass + + +async def get_session() -> AsyncSession: + async with async_session() as session: + yield session diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..6ea4c70 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,71 @@ +import hashlib +import hmac +import json +import time +from datetime import datetime, timedelta, timezone +from urllib.parse import parse_qs, unquote + +import jwt + + +def verify_telegram_login(data: dict, bot_token: str) -> bool: + """Verify data from Telegram Login Widget.""" + data = dict(data) + check_hash = data.pop("hash", "") + if not check_hash: + return False + + data_check_string = "\n".join( + f"{k}={v}" for k, v in sorted(data.items()) + ) + secret_key = hashlib.sha256(bot_token.encode()).digest() + computed = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() + + if int(data.get("auth_date", 0)) < time.time() - 86400: + return False + + return hmac.compare_digest(computed, check_hash) + + +def verify_telegram_webapp(init_data: str, bot_token: str) -> dict | None: + """Verify Telegram WebApp initData and return parsed user dict.""" + parsed = parse_qs(init_data) + data = {k: v[0] for k, v in parsed.items()} + + check_hash = data.pop("hash", "") + if not check_hash: + return None + + data_check_string = "\n".join( + f"{k}={v}" for k, v in sorted(data.items()) + ) + secret_key = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest() + computed = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() + + if not hmac.compare_digest(computed, check_hash): + return None + + user_raw = data.get("user") + if not user_raw: + return None + + return json.loads(unquote(user_raw)) + + +def create_access_token( + rider_id: str, + telegram_id: int, + secret: str, + algorithm: str, + expires_minutes: int, +) -> str: + payload = { + "sub": rider_id, + "tg_id": telegram_id, + "exp": datetime.now(timezone.utc) + timedelta(minutes=expires_minutes), + } + return jwt.encode(payload, secret, algorithm=algorithm) + + +def decode_access_token(token: str, secret: str, algorithm: str) -> dict: + return jwt.decode(token, secret, algorithms=[algorithm]) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..e68d177 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,37 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from backend.app.api.router import api_router +from backend.app.core.config import settings + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + yield + # Shutdown + + +app = FastAPI( + title="VeloBrain", + description="AI-Powered Cycling Training Platform", + version="0.1.0", + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router) + + +@app.get("/health") +async def health(): + return {"status": "ok"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..d72ea56 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,16 @@ +from backend.app.models.rider import Rider +from backend.app.models.activity import Activity, ActivityMetrics, DataPoint, Interval +from backend.app.models.fitness import FitnessHistory, PowerCurve, DiaryEntry +from backend.app.models.training import TrainingPlan + +__all__ = [ + "Rider", + "Activity", + "ActivityMetrics", + "DataPoint", + "Interval", + "FitnessHistory", + "PowerCurve", + "DiaryEntry", + "TrainingPlan", +] diff --git a/backend/app/models/activity.py b/backend/app/models/activity.py new file mode 100644 index 0000000..e87e33f --- /dev/null +++ b/backend/app/models/activity.py @@ -0,0 +1,84 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, Float, Integer, DateTime, ForeignKey, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.app.core.database import Base + + +class Activity(Base): + __tablename__ = "activities" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + rider_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("riders.id")) + name: Mapped[str | None] = mapped_column(String(200), nullable=True) + activity_type: Mapped[str] = mapped_column(String(50), default="road") + date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + duration: Mapped[int] = mapped_column(Integer) # seconds + distance: Mapped[float | None] = mapped_column(Float, nullable=True) # meters + elevation_gain: Mapped[float | None] = mapped_column(Float, nullable=True) # meters + file_path: Mapped[str | None] = mapped_column(String(500), nullable=True) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + rider = relationship("Rider", back_populates="activities") + metrics = relationship("ActivityMetrics", back_populates="activity", uselist=False, lazy="joined") + intervals = relationship("Interval", back_populates="activity", lazy="selectin") + data_points = relationship("DataPoint", back_populates="activity", lazy="noload") + + +class ActivityMetrics(Base): + __tablename__ = "activity_metrics" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + activity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("activities.id"), unique=True) + + tss: Mapped[float | None] = mapped_column(Float, nullable=True) + normalized_power: Mapped[float | None] = mapped_column(Float, nullable=True) + intensity_factor: Mapped[float | None] = mapped_column(Float, nullable=True) + variability_index: Mapped[float | None] = mapped_column(Float, nullable=True) + avg_power: Mapped[float | None] = mapped_column(Float, nullable=True) + max_power: Mapped[int | None] = mapped_column(Integer, nullable=True) + avg_hr: Mapped[int | None] = mapped_column(Integer, nullable=True) + max_hr: Mapped[int | None] = mapped_column(Integer, nullable=True) + avg_cadence: Mapped[int | None] = mapped_column(Integer, nullable=True) + avg_speed: Mapped[float | None] = mapped_column(Float, nullable=True) # m/s + calories: Mapped[int | None] = mapped_column(Integer, nullable=True) + + activity = relationship("Activity", back_populates="metrics") + + +class DataPoint(Base): + __tablename__ = "data_points" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + activity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("activities.id")) + timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + + power: Mapped[int | None] = mapped_column(Integer, nullable=True) + heart_rate: Mapped[int | None] = mapped_column(Integer, nullable=True) + cadence: Mapped[int | None] = mapped_column(Integer, nullable=True) + speed: Mapped[float | None] = mapped_column(Float, nullable=True) + latitude: Mapped[float | None] = mapped_column(Float, nullable=True) + longitude: Mapped[float | None] = mapped_column(Float, nullable=True) + altitude: Mapped[float | None] = mapped_column(Float, nullable=True) + temperature: Mapped[int | None] = mapped_column(Integer, nullable=True) + + activity = relationship("Activity", back_populates="data_points") + + +class Interval(Base): + __tablename__ = "intervals" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + activity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("activities.id")) + start_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + end_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + interval_type: Mapped[str] = mapped_column(String(50)) # work / rest / climb + avg_power: Mapped[float | None] = mapped_column(Float, nullable=True) + avg_hr: Mapped[int | None] = mapped_column(Integer, nullable=True) + duration: Mapped[int | None] = mapped_column(Integer, nullable=True) # seconds + + activity = relationship("Activity", back_populates="intervals") diff --git a/backend/app/models/fitness.py b/backend/app/models/fitness.py new file mode 100644 index 0000000..3b4d0a3 --- /dev/null +++ b/backend/app/models/fitness.py @@ -0,0 +1,42 @@ +import uuid +from datetime import date, datetime + +from sqlalchemy import String, Float, Integer, Date, DateTime, ForeignKey, Text, func +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from backend.app.core.database import Base + + +class FitnessHistory(Base): + __tablename__ = "fitness_history" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + rider_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("riders.id")) + date: Mapped[date] = mapped_column(Date, index=True) + ctl: Mapped[float] = mapped_column(Float, default=0) + atl: Mapped[float] = mapped_column(Float, default=0) + tsb: Mapped[float] = mapped_column(Float, default=0) + ramp_rate: Mapped[float | None] = mapped_column(Float, nullable=True) + + +class PowerCurve(Base): + __tablename__ = "power_curves" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + activity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("activities.id")) + curve_data: Mapped[dict] = mapped_column(JSONB) # {duration_seconds: max_power} + + +class DiaryEntry(Base): + __tablename__ = "diary_entries" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + activity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("activities.id"), unique=True) + ai_summary: Mapped[str | None] = mapped_column(Text, nullable=True) + rider_notes: Mapped[str | None] = mapped_column(Text, nullable=True) + mood: Mapped[str | None] = mapped_column(String(50), nullable=True) + rpe: Mapped[int | None] = mapped_column(Integer, nullable=True) + sleep_hours: Mapped[float | None] = mapped_column(Float, nullable=True) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/app/models/rider.py b/backend/app/models/rider.py new file mode 100644 index 0000000..3e28a5a --- /dev/null +++ b/backend/app/models/rider.py @@ -0,0 +1,29 @@ +import uuid +from datetime import datetime + +from sqlalchemy import String, Float, BigInteger, DateTime, func +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.app.core.database import Base + + +class Rider(Base): + __tablename__ = "riders" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + telegram_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, index=True, nullable=True) + telegram_username: Mapped[str | None] = mapped_column(String(100), nullable=True) + avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True) + name: Mapped[str] = mapped_column(String(100)) + ftp: Mapped[float | None] = mapped_column(Float, nullable=True) + lthr: Mapped[int | None] = mapped_column(nullable=True) + weight: Mapped[float | None] = mapped_column(Float, nullable=True) + zones_config: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + goals: Mapped[str | None] = mapped_column(String(500), nullable=True) + experience_level: Mapped[str | None] = mapped_column(String(50), nullable=True) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + activities = relationship("Activity", back_populates="rider", lazy="selectin") diff --git a/backend/app/models/training.py b/backend/app/models/training.py new file mode 100644 index 0000000..60dedf6 --- /dev/null +++ b/backend/app/models/training.py @@ -0,0 +1,23 @@ +import uuid +from datetime import date, datetime + +from sqlalchemy import String, Date, DateTime, ForeignKey, Text, func +from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from backend.app.core.database import Base + + +class TrainingPlan(Base): + __tablename__ = "training_plans" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + rider_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("riders.id")) + goal: Mapped[str] = mapped_column(String(200)) + start_date: Mapped[date] = mapped_column(Date) + end_date: Mapped[date] = mapped_column(Date) + phase: Mapped[str | None] = mapped_column(String(50), nullable=True) + weeks_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/activity.py b/backend/app/schemas/activity.py new file mode 100644 index 0000000..2f66b43 --- /dev/null +++ b/backend/app/schemas/activity.py @@ -0,0 +1,52 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + + +class ActivityMetricsResponse(BaseModel): + model_config = {"from_attributes": True} + + tss: float | None = None + normalized_power: float | None = None + intensity_factor: float | None = None + variability_index: float | None = None + avg_power: float | None = None + max_power: int | None = None + avg_hr: int | None = None + max_hr: int | None = None + avg_cadence: int | None = None + avg_speed: float | None = None + + +class ActivityResponse(BaseModel): + model_config = {"from_attributes": True} + + id: UUID + rider_id: UUID + name: str | None = None + activity_type: str + date: datetime + duration: int + distance: float | None = None + elevation_gain: float | None = None + metrics: ActivityMetricsResponse | None = None + + +class ActivityListResponse(BaseModel): + items: list[ActivityResponse] + total: int + + +class DataPointResponse(BaseModel): + model_config = {"from_attributes": True} + + timestamp: datetime + power: int | None = None + heart_rate: int | None = None + cadence: int | None = None + speed: float | None = None + latitude: float | None = None + longitude: float | None = None + altitude: float | None = None + temperature: int | None = None diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..9a4f442 --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + +from backend.app.schemas.rider import RiderResponse + + +class TelegramLoginRequest(BaseModel): + id: int + first_name: str + last_name: str | None = None + username: str | None = None + photo_url: str | None = None + auth_date: int + hash: str + + +class TelegramWebAppRequest(BaseModel): + init_data: str + + +class AuthResponse(BaseModel): + access_token: str + token_type: str = "bearer" + rider: RiderResponse diff --git a/backend/app/schemas/rider.py b/backend/app/schemas/rider.py new file mode 100644 index 0000000..58f05af --- /dev/null +++ b/backend/app/schemas/rider.py @@ -0,0 +1,38 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class RiderCreate(BaseModel): + name: str + ftp: float | None = None + lthr: int | None = None + weight: float | None = None + goals: str | None = None + experience_level: str | None = None + + +class RiderUpdate(BaseModel): + name: str | None = None + ftp: float | None = None + lthr: int | None = None + weight: float | None = None + zones_config: dict | None = None + goals: str | None = None + experience_level: str | None = None + + +class RiderResponse(BaseModel): + model_config = {"from_attributes": True} + + id: UUID + telegram_id: int | None = None + telegram_username: str | None = None + avatar_url: str | None = None + name: str + ftp: float | None = None + lthr: int | None = None + weight: float | None = None + zones_config: dict | None = None + goals: str | None = None + experience_level: str | None = None diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/fit_parser.py b/backend/app/services/fit_parser.py new file mode 100644 index 0000000..48ef6f0 --- /dev/null +++ b/backend/app/services/fit_parser.py @@ -0,0 +1,102 @@ +import uuid +from datetime import datetime, timezone +from io import BytesIO + +import fitdecode + +from backend.app.models.activity import Activity, DataPoint + + +def parse_fit_file( + file_content: bytes, + rider_id: uuid.UUID, + file_path: str, +) -> tuple[Activity, list[DataPoint]]: + """Parse a .FIT file and return an Activity with its DataPoints.""" + data_points: list[DataPoint] = [] + session_data: dict = {} + + with fitdecode.FitReader(BytesIO(file_content)) as fit: + for frame in fit: + if not isinstance(frame, fitdecode.FitDataMessage): + continue + + if frame.name == "record": + dp = _parse_record(frame) + if dp: + data_points.append(dp) + + elif frame.name == "session": + session_data = _parse_session(frame) + + start_time = data_points[0].timestamp if data_points else datetime.now(timezone.utc) + end_time = data_points[-1].timestamp if data_points else start_time + duration = int((end_time - start_time).total_seconds()) if data_points else 0 + + activity = Activity( + rider_id=rider_id, + name=session_data.get("sport", "Ride"), + activity_type=session_data.get("sub_sport", "road"), + date=start_time, + duration=duration, + distance=session_data.get("total_distance"), + elevation_gain=session_data.get("total_ascent"), + file_path=file_path, + ) + + return activity, data_points + + +def _parse_record(frame: fitdecode.FitDataMessage) -> DataPoint | None: + """Parse a single record message into a DataPoint.""" + timestamp = _get_field(frame, "timestamp") + if not timestamp: + return None + + if isinstance(timestamp, datetime) and timestamp.tzinfo is None: + timestamp = timestamp.replace(tzinfo=timezone.utc) + + return DataPoint( + timestamp=timestamp, + power=_get_field(frame, "power"), + heart_rate=_get_field(frame, "heart_rate"), + cadence=_get_field(frame, "cadence"), + speed=_get_field(frame, "speed"), + latitude=_semicircles_to_degrees(_get_field(frame, "position_lat")), + longitude=_semicircles_to_degrees(_get_field(frame, "position_long")), + altitude=_get_field(frame, "altitude"), + temperature=_get_field(frame, "temperature"), + ) + + +def _parse_session(frame: fitdecode.FitDataMessage) -> dict: + """Extract session-level data from FIT session message.""" + return { + "sport": _get_field_str(frame, "sport"), + "sub_sport": _get_field_str(frame, "sub_sport"), + "total_distance": _get_field(frame, "total_distance"), + "total_ascent": _get_field(frame, "total_ascent"), + "total_elapsed_time": _get_field(frame, "total_elapsed_time"), + } + + +def _get_field(frame: fitdecode.FitDataMessage, name: str): + """Safely get a field value from a FIT frame.""" + try: + field = frame.get_field(name) + return field.value if field else None + except KeyError: + return None + + +def _get_field_str(frame: fitdecode.FitDataMessage, name: str) -> str | None: + """Get field value as string.""" + val = _get_field(frame, name) + return str(val) if val is not None else None + + +def _semicircles_to_degrees(semicircles: int | None) -> float | None: + """Convert Garmin semicircles to decimal degrees.""" + if semicircles is None: + return None + return semicircles * (180.0 / 2**31) diff --git a/backend/app/services/gemini_client.py b/backend/app/services/gemini_client.py new file mode 100644 index 0000000..878997d --- /dev/null +++ b/backend/app/services/gemini_client.py @@ -0,0 +1,126 @@ +from google import genai +from google.genai import types + +from backend.app.core.config import settings + +_client: genai.Client | None = None + + +def get_client() -> genai.Client: + global _client + if _client is None: + _client = genai.Client(api_key=settings.GEMINI_API_KEY) + return _client + + +def chat_sync( + messages: list[dict[str, str]], + system_instruction: str | None = None, + temperature: float = 0.7, + max_tokens: int = 8192, +) -> str: + """ + Synchronous chat with Gemini. + + messages: list of {"role": "user"|"model", "text": "..."} + Returns the model's text response. + """ + client = get_client() + + contents = [ + types.Content( + role=m["role"], + parts=[types.Part.from_text(text=m["text"])], + ) + for m in messages + ] + + config = types.GenerateContentConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ) + if system_instruction: + config.system_instruction = system_instruction + + response = client.models.generate_content( + model=settings.GEMINI_MODEL, + contents=contents, + config=config, + ) + + return response.text or "" + + +async def chat_async( + messages: list[dict[str, str]], + system_instruction: str | None = None, + temperature: float = 0.7, + max_tokens: int = 8192, +) -> str: + """ + Async chat with Gemini. + + messages: list of {"role": "user"|"model", "text": "..."} + Returns the model's text response. + """ + client = get_client() + + contents = [ + types.Content( + role=m["role"], + parts=[types.Part.from_text(text=m["text"])], + ) + for m in messages + ] + + config = types.GenerateContentConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ) + if system_instruction: + config.system_instruction = system_instruction + + response = await client.aio.models.generate_content( + model=settings.GEMINI_MODEL, + contents=contents, + config=config, + ) + + return response.text or "" + + +async def chat_stream( + messages: list[dict[str, str]], + system_instruction: str | None = None, + temperature: float = 0.7, + max_tokens: int = 8192, +): + """ + Async streaming chat with Gemini. Yields text chunks. + + messages: list of {"role": "user"|"model", "text": "..."} + """ + client = get_client() + + contents = [ + types.Content( + role=m["role"], + parts=[types.Part.from_text(text=m["text"])], + ) + for m in messages + ] + + config = types.GenerateContentConfig( + temperature=temperature, + max_output_tokens=max_tokens, + ) + if system_instruction: + config.system_instruction = system_instruction + + async for chunk in client.aio.models.generate_content_stream( + model=settings.GEMINI_MODEL, + contents=contents, + config=config, + ): + if chunk.text: + yield chunk.text diff --git a/backend/app/services/metrics.py b/backend/app/services/metrics.py new file mode 100644 index 0000000..b61a156 --- /dev/null +++ b/backend/app/services/metrics.py @@ -0,0 +1,83 @@ +import uuid + +import numpy as np +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.models.activity import Activity, ActivityMetrics, DataPoint +from backend.app.models.rider import Rider + + +def calculate_metrics( + data_points: list[DataPoint], + activity: Activity, + rider_id: uuid.UUID, + session: AsyncSession, +) -> ActivityMetrics | None: + """Calculate power-based metrics for an activity.""" + if not data_points: + return None + + powers = np.array([dp.power for dp in data_points if dp.power is not None], dtype=float) + hrs = np.array([dp.heart_rate for dp in data_points if dp.heart_rate is not None], dtype=float) + cadences = np.array([dp.cadence for dp in data_points if dp.cadence is not None], dtype=float) + speeds = np.array([dp.speed for dp in data_points if dp.speed is not None], dtype=float) + + avg_power = float(np.mean(powers)) if len(powers) > 0 else None + max_power = int(np.max(powers)) if len(powers) > 0 else None + np_value = _normalized_power(powers) if len(powers) >= 30 else avg_power + + avg_hr = int(np.mean(hrs)) if len(hrs) > 0 else None + max_hr = int(np.max(hrs)) if len(hrs) > 0 else None + avg_cadence = int(np.mean(cadences)) if len(cadences) > 0 else None + avg_speed = float(np.mean(speeds)) if len(speeds) > 0 else None + + # IF, VI, TSS require FTP — will be None if no FTP set + intensity_factor = None + variability_index = None + tss = None + + if np_value and avg_power and avg_power > 0: + variability_index = np_value / avg_power + + return ActivityMetrics( + activity_id=activity.id, + tss=tss, + normalized_power=round(np_value, 1) if np_value else None, + intensity_factor=intensity_factor, + variability_index=round(variability_index, 2) if variability_index else None, + avg_power=round(avg_power, 1) if avg_power else None, + max_power=max_power, + avg_hr=avg_hr, + max_hr=max_hr, + avg_cadence=avg_cadence, + avg_speed=round(avg_speed, 2) if avg_speed else None, + ) + + +def calculate_metrics_with_ftp( + metrics: ActivityMetrics, + ftp: float, + duration_seconds: int, +) -> ActivityMetrics: + """Enrich metrics with FTP-dependent values (IF, TSS).""" + if metrics.normalized_power and ftp > 0: + metrics.intensity_factor = round(metrics.normalized_power / ftp, 2) + metrics.tss = round( + (duration_seconds * metrics.normalized_power * metrics.intensity_factor) + / (ftp * 3600) + * 100, + 1, + ) + return metrics + + +def _normalized_power(powers: np.ndarray) -> float: + """ + NP = 4th root of mean of 4th powers of 30s rolling average. + """ + if len(powers) < 30: + return float(np.mean(powers)) + + rolling = np.convolve(powers, np.ones(30) / 30, mode="valid") + return float(np.power(np.mean(np.power(rolling, 4)), 0.25)) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..d7c9deb --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,34 @@ +# Web framework +fastapi==0.115.12 +uvicorn[standard]==0.34.2 +python-multipart==0.0.20 + +# Database +sqlalchemy[asyncio]==2.0.41 +asyncpg==0.30.0 +alembic==1.15.2 + +# FIT parsing +fitdecode==0.10.0 + +# Analytics +numpy==2.2.4 +pandas==2.2.3 + +# AI +anthropic==0.52.0 +google-genai==1.67.0 + +# Config +pydantic-settings==2.9.1 + +# Telegram bot +aiogram==3.20.0 + +# Auth +PyJWT==2.10.1 + +# Testing +pytest==8.3.5 +pytest-asyncio==0.25.3 +httpx==0.28.1 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.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/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ad44e18 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2490 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@primeuix/themes": "^2.0.3", + "@types/leaflet": "^1.9.21", + "axios": "^1.13.6", + "echarts": "^6.0.0", + "leaflet": "^1.9.4", + "pinia": "^3.0.4", + "primeicons": "^7.0.0", + "primevue": "^4.5.4", + "tailwindcss-primeui": "^0.6.1", + "vue": "^3.5.30", + "vue-echarts": "^8.0.1", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.1", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.0", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "vite": "^8.0.0", + "vue-tsc": "^3.2.5" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "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/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@primeuix/styled": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", + "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styles": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz", + "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4" + } + }, + "node_modules/@primeuix/themes": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.3.tgz", + "integrity": "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", + "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.4.tgz", + "integrity": "sha512-lYJJB3wTrDJ8MkLctzHfrPZAqXVxoatjIsswSJzupatf6ZogJHVYADUKcn1JAkLLk8dtV1FA2AxDek663fHO5Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/utils": "^0.6.2" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.4.tgz", + "integrity": "sha512-DxgryEc7ZmUqcEhYMcxGBRyFzdtLIoy3jLtlH1zsVSRZaG+iSAcjQ88nvfkZxGUZtZBFL7sRjF6KLq3bJZJwUw==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "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.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "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/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "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/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "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/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "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/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "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/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "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/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "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/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "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/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "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/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "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/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "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/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "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.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/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.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", + "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.0.tgz", + "integrity": "sha512-RP+v9Cpbsk1ZVXltCHHkYBr7+624x6gcijJXVjIcsYk7JXqvIpRtMwU2ARLvWDhmy9ffdFYxhsfJnPztADBohQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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/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/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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==", + "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/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "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/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "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/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/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/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/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-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/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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/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==", + "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/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "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==", + "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/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primevue": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.4.tgz", + "integrity": "sha512-nTyEohZABFJhVIpeUxgP0EJ8vKcJAhD+Z7DYj95e7ie/MNUCjRNcGjqmE1cXtXi4z54qDfTSI9h2uJ51qz2DIw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/styles": "^2.0.2", + "@primeuix/utils": "^0.6.2", + "@primevue/core": "4.5.4", + "@primevue/icons": "4.5.4" + }, + "engines": { + "node": ">=12.11.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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss-primeui": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/tailwindcss-primeui/-/tailwindcss-primeui-0.6.1.tgz", + "integrity": "sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.1.0" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/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/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/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "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", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-echarts": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-8.0.1.tgz", + "integrity": "sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==", + "license": "MIT", + "peerDependencies": { + "echarts": "^6.0.0", + "vue": "^3.3.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", + "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.5" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..967b470 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@primeuix/themes": "^2.0.3", + "@types/leaflet": "^1.9.21", + "axios": "^1.13.6", + "echarts": "^6.0.0", + "leaflet": "^1.9.4", + "pinia": "^3.0.4", + "primeicons": "^7.0.0", + "primevue": "^4.5.4", + "tailwindcss-primeui": "^0.6.1", + "vue": "^3.5.30", + "vue-echarts": "^8.0.1", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.1", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.0", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "vite": "^8.0.0", + "vue-tsc": "^3.2.5" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..48b0c1f --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,31 @@ + + + diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..cc51a3d20ad4bc961b596a6adfd686685cd84bb0 GIT binary patch literal 44919 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 JPDHLkV1myZcL)Fg literal 0 HcmV?d00001 diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/composables/useApi.ts b/frontend/src/composables/useApi.ts new file mode 100644 index 0000000..a22ef1a --- /dev/null +++ b/frontend/src/composables/useApi.ts @@ -0,0 +1,28 @@ +import axios from 'axios' + +const api = axios.create({ + baseURL: '/api', +}) + +api.interceptors.request.use((config) => { + const token = localStorage.getItem('access_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('access_token') + window.location.href = '/login' + } + return Promise.reject(error) + }, +) + +export function useApi() { + return { api } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..f90f00d --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,26 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import PrimeVue from 'primevue/config' +import Aura from '@primeuix/themes/aura' +import router from './router' +import App from './App.vue' +import './style.css' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(PrimeVue, { + theme: { + preset: Aura, + options: { + cssLayer: { + name: 'primevue', + order: 'theme, base, primevue', + }, + darkModeSelector: '.app-dark', + }, + }, +}) + +app.mount('#app') diff --git a/frontend/src/router.ts b/frontend/src/router.ts new file mode 100644 index 0000000..9dea6c9 --- /dev/null +++ b/frontend/src/router.ts @@ -0,0 +1,52 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from './stores/auth' + +const routes = [ + { + path: '/login', + name: 'login', + component: () => import('./views/LoginView.vue'), + meta: { requiresAuth: false }, + }, + { + path: '/', + name: 'dashboard', + component: () => import('./views/DashboardView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/activities', + name: 'activities', + component: () => import('./views/ActivitiesView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/activities/:id', + name: 'activity-detail', + component: () => import('./views/ActivityDetailView.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/settings', + name: 'settings', + component: () => import('./views/SettingsView.vue'), + meta: { requiresAuth: true }, + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +router.beforeEach((to) => { + const auth = useAuthStore() + if (to.meta.requiresAuth !== false && !auth.isAuthenticated) { + return { name: 'login' } + } + if (to.name === 'login' && auth.isAuthenticated) { + return { name: 'dashboard' } + } +}) + +export default router diff --git a/frontend/src/stores/activities.ts b/frontend/src/stores/activities.ts new file mode 100644 index 0000000..878c389 --- /dev/null +++ b/frontend/src/stores/activities.ts @@ -0,0 +1,32 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { useApi } from '../composables/useApi' +import type { Activity } from '../types/models' + +export const useActivitiesStore = defineStore('activities', () => { + const { api } = useApi() + const activities = ref([]) + const total = ref(0) + + async function fetchActivities(riderId: string, limit = 20, offset = 0) { + const { data } = await api.get('/activities', { + params: { rider_id: riderId, limit, offset }, + }) + activities.value = data.items + total.value = data.total + } + + async function uploadFit(riderId: string, file: File) { + const form = new FormData() + form.append('file', file) + const { data } = await api.post( + `/activities/upload?rider_id=${riderId}`, + form, + ) + activities.value.unshift(data) + total.value++ + return data + } + + return { activities, total, fetchActivities, uploadFit } +}) diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..23c321b --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,45 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApi } from '../composables/useApi' +import type { Rider } from '../types/models' +import router from '../router' + +interface AuthResponse { + access_token: string + token_type: string + rider: Rider +} + +export const useAuthStore = defineStore('auth', () => { + const { api } = useApi() + const token = ref(localStorage.getItem('access_token')) + const rider = ref(null) + const isAuthenticated = computed(() => !!token.value) + + function setAuth(response: AuthResponse) { + token.value = response.access_token + rider.value = response.rider + localStorage.setItem('access_token', response.access_token) + } + + function logout() { + token.value = null + rider.value = null + localStorage.removeItem('access_token') + router.push({ name: 'login' }) + } + + async function loginWithTelegram(data: TelegramLoginData) { + const { data: response } = await api.post('/auth/telegram-login', data) + setAuth(response) + router.push({ name: 'dashboard' }) + } + + async function loginWithWebApp(initData: string) { + const { data: response } = await api.post('/auth/telegram-webapp', { init_data: initData }) + setAuth(response) + router.push({ name: 'dashboard' }) + } + + return { token, rider, isAuthenticated, loginWithTelegram, loginWithWebApp, logout } +}) diff --git a/frontend/src/stores/rider.ts b/frontend/src/stores/rider.ts new file mode 100644 index 0000000..d458c99 --- /dev/null +++ b/frontend/src/stores/rider.ts @@ -0,0 +1,21 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { useApi } from '../composables/useApi' +import type { Rider } from '../types/models' + +export const useRiderStore = defineStore('rider', () => { + const { api } = useApi() + const rider = ref(null) + + async function fetchRider(id: string) { + const { data } = await api.get(`/rider/profile/${id}`) + rider.value = data + } + + async function updateRider(id: string, updates: Partial) { + const { data } = await api.put(`/rider/profile/${id}`, updates) + rider.value = data + } + + return { rider, fetchRider, updateRider } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..e7187bb --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,3 @@ +@import "tailwindcss"; +@plugin "tailwindcss-primeui"; +@import "primeicons/primeicons.css"; diff --git a/frontend/src/types/models.ts b/frontend/src/types/models.ts new file mode 100644 index 0000000..b7550fc --- /dev/null +++ b/frontend/src/types/models.ts @@ -0,0 +1,50 @@ +export interface Rider { + id: string + telegram_id: number | null + telegram_username: string | null + avatar_url: string | null + name: string + ftp: number | null + lthr: number | null + weight: number | null + zones_config: Record | null + goals: string | null + experience_level: string | null +} + +export interface ActivityMetrics { + tss: number | null + normalized_power: number | null + intensity_factor: number | null + variability_index: number | null + avg_power: number | null + max_power: number | null + avg_hr: number | null + max_hr: number | null + avg_cadence: number | null + avg_speed: number | null +} + +export interface Activity { + id: string + rider_id: string + name: string | null + activity_type: string + date: string + duration: number + distance: number | null + elevation_gain: number | null + metrics: ActivityMetrics | null +} + +export interface DataPoint { + timestamp: string + power: number | null + heart_rate: number | null + cadence: number | null + speed: number | null + latitude: number | null + longitude: number | null + altitude: number | null + temperature: number | null +} diff --git a/frontend/src/types/telegram.d.ts b/frontend/src/types/telegram.d.ts new file mode 100644 index 0000000..321c950 --- /dev/null +++ b/frontend/src/types/telegram.d.ts @@ -0,0 +1,31 @@ +interface TelegramWebApp { + initData: string + initDataUnsafe: { + user?: { + id: number + first_name: string + last_name?: string + username?: string + photo_url?: string + } + } + ready(): void + close(): void + expand(): void +} + +interface Window { + Telegram?: { + WebApp?: TelegramWebApp + } +} + +interface TelegramLoginData { + id: number + first_name: string + last_name?: string + username?: string + photo_url?: string + auth_date: number + hash: string +} diff --git a/frontend/src/views/ActivitiesView.vue b/frontend/src/views/ActivitiesView.vue new file mode 100644 index 0000000..67e624f --- /dev/null +++ b/frontend/src/views/ActivitiesView.vue @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/views/ActivityDetailView.vue b/frontend/src/views/ActivityDetailView.vue new file mode 100644 index 0000000..1496f49 --- /dev/null +++ b/frontend/src/views/ActivityDetailView.vue @@ -0,0 +1,39 @@ + + + diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue new file mode 100644 index 0000000..d4d8a65 --- /dev/null +++ b/frontend/src/views/DashboardView.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..00aa54b --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,78 @@ + + + diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue new file mode 100644 index 0000000..73ad30f --- /dev/null +++ b/frontend/src/views/SettingsView.vue @@ -0,0 +1,14 @@ + + + diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..8d16e42 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/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/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..80b43cd --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/velobrain_architecture.docx b/velobrain_architecture.docx new file mode 100644 index 0000000000000000000000000000000000000000..9f473f6ba90fe7d38ba0f4456304fe32d2f5d73f GIT binary patch literal 22981 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 literal 0 HcmV?d00001