From 39fdd43d8e5eeedf204e14bd963313c8d28d1daa Mon Sep 17 00:00:00 2001 From: Andrey Parovin Date: Sun, 11 Oct 2026 14:12:10 +0300 Subject: [PATCH] initial commit --- .gitignore | 16 + README.md | 41 + backend/.dockerignore | 4 + backend/Dockerfile | 14 + backend/app/__init__.py | 0 backend/app/config.py | 24 + backend/app/data/program.json | 1689 +++++++++++ backend/app/database.py | 21 + backend/app/main.py | 32 + backend/app/models.py | 118 + backend/app/routers/__init__.py | 0 backend/app/routers/actions.py | 159 + backend/app/routers/auth.py | 79 + backend/app/routers/workouts.py | 366 +++ backend/app/schemas.py | 193 ++ backend/app/services/__init__.py | 0 backend/app/services/auth.py | 56 + backend/app/services/program.py | 23 + backend/app/services/reschedule.py | 114 + backend/app/services/schedule.py | 197 ++ backend/app/services/weights.py | 33 + backend/app/services/workout.py | 589 ++++ backend/requirements.txt | 10 + docker-compose.yml | 36 + frontend/.dockerignore | 4 + frontend/.env.android | 2 + frontend/.gitignore | 24 + frontend/.oxlintrc.json | 8 + frontend/ANDROID.md | 32 + frontend/Dockerfile | 11 + frontend/README.md | 32 + frontend/android/.gitignore | 101 + frontend/android/app/.gitignore | 2 + frontend/android/app/build.gradle | 54 + frontend/android/app/capacitor.build.gradle | 19 + frontend/android/app/proguard-rules.pro | 21 + .../myapp/ExampleInstrumentedTest.java | 26 + .../android/app/src/main/AndroidManifest.xml | 42 + .../com/benchpress/secret/MainActivity.java | 15 + .../main/res/drawable-land-hdpi/splash.png | Bin 0 -> 7705 bytes .../main/res/drawable-land-mdpi/splash.png | Bin 0 -> 4040 bytes .../main/res/drawable-land-xhdpi/splash.png | Bin 0 -> 9251 bytes .../main/res/drawable-land-xxhdpi/splash.png | Bin 0 -> 13984 bytes .../main/res/drawable-land-xxxhdpi/splash.png | Bin 0 -> 17683 bytes .../main/res/drawable-port-hdpi/splash.png | Bin 0 -> 7934 bytes .../main/res/drawable-port-mdpi/splash.png | Bin 0 -> 4096 bytes .../main/res/drawable-port-xhdpi/splash.png | Bin 0 -> 9875 bytes .../main/res/drawable-port-xxhdpi/splash.png | Bin 0 -> 13346 bytes .../main/res/drawable-port-xxxhdpi/splash.png | Bin 0 -> 17489 bytes .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 ++ .../app/src/main/res/drawable/splash.png | Bin 0 -> 4040 bytes .../app/src/main/res/layout/activity_main.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3363 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 10874 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3363 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1988 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 6180 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1988 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4891 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 16449 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4891 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8357 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 29410 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8357 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 12236 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 43268 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12236 bytes .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 7 + .../app/src/main/res/values/styles.xml | 25 + .../app/src/main/res/xml/file_paths.xml | 5 + .../main/res/xml/network_security_config.xml | 13 + .../getcapacitor/myapp/ExampleUnitTest.java | 18 + frontend/android/build-apk.ps1 | 54 + frontend/android/build.gradle | 29 + frontend/android/capacitor.settings.gradle | 3 + frontend/android/gradle.properties | 22 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + frontend/android/gradlew | 251 ++ frontend/android/gradlew.bat | 94 + frontend/android/settings.gradle | 5 + frontend/android/variables.gradle | 16 + frontend/capacitor.config.ts | 15 + frontend/index.html | 14 + frontend/nginx.conf | 17 + frontend/package-lock.json | 2617 +++++++++++++++++ frontend/package.json | 33 + frontend/public/favicon.png | Bin 0 -> 1214 bytes frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/scripts/build-apk.ps1 | 7 + frontend/scripts/install-emulator.ps1 | 65 + frontend/scripts/update-app.ps1 | 12 + frontend/src/App.css | 184 ++ frontend/src/App.tsx | 57 + frontend/src/api.ts | 172 ++ frontend/src/apiTypes.ts | 75 + frontend/src/assets/hero.png | Bin 0 -> 13057 bytes frontend/src/assets/react.svg | 1 + frontend/src/assets/vite.svg | 1 + frontend/src/auth.tsx | 80 + frontend/src/components/AppCalendar.tsx | 80 + frontend/src/components/WeightStepper.tsx | 45 + frontend/src/data/program.json | 1689 +++++++++++ frontend/src/index.css | 844 ++++++ frontend/src/local/api.ts | 711 +++++ frontend/src/local/db.ts | 51 + frontend/src/local/reschedule.ts | 132 + frontend/src/local/schedule.ts | 217 ++ frontend/src/local/types.ts | 71 + frontend/src/local/weights.ts | 21 + frontend/src/main.tsx | 13 + frontend/src/pages/HomePage.tsx | 314 ++ frontend/src/pages/LoginPage.tsx | 67 + frontend/src/pages/OnboardingPage.tsx | 207 ++ frontend/src/pages/SettingsPage.tsx | 209 ++ frontend/src/pages/WorkoutPage.tsx | 807 +++++ frontend/src/vite-env.d.ts | 10 + frontend/tsconfig.app.json | 27 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 23 + frontend/vite.config.ts | 13 + Драфт.xlsx | Bin 0 -> 22896 bytes 127 files changed, 13919 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/data/program.json create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models.py create mode 100644 backend/app/routers/__init__.py create mode 100644 backend/app/routers/actions.py create mode 100644 backend/app/routers/auth.py create mode 100644 backend/app/routers/workouts.py create mode 100644 backend/app/schemas.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/auth.py create mode 100644 backend/app/services/program.py create mode 100644 backend/app/services/reschedule.py create mode 100644 backend/app/services/schedule.py create mode 100644 backend/app/services/weights.py create mode 100644 backend/app/services/workout.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/.env.android create mode 100644 frontend/.gitignore create mode 100644 frontend/.oxlintrc.json create mode 100644 frontend/ANDROID.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/android/.gitignore create mode 100644 frontend/android/app/.gitignore create mode 100644 frontend/android/app/build.gradle create mode 100644 frontend/android/app/capacitor.build.gradle create mode 100644 frontend/android/app/proguard-rules.pro create mode 100644 frontend/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java create mode 100644 frontend/android/app/src/main/AndroidManifest.xml create mode 100644 frontend/android/app/src/main/java/com/benchpress/secret/MainActivity.java create mode 100644 frontend/android/app/src/main/res/drawable-land-hdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-land-mdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-land-xhdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-land-xxhdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-land-xxxhdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-port-hdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-port-mdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-port-xhdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-port-xxhdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-port-xxxhdpi/splash.png create mode 100644 frontend/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 frontend/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 frontend/android/app/src/main/res/drawable/splash.png create mode 100644 frontend/android/app/src/main/res/layout/activity_main.xml create mode 100644 frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 frontend/android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 frontend/android/app/src/main/res/values/strings.xml create mode 100644 frontend/android/app/src/main/res/values/styles.xml create mode 100644 frontend/android/app/src/main/res/xml/file_paths.xml create mode 100644 frontend/android/app/src/main/res/xml/network_security_config.xml create mode 100644 frontend/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java create mode 100644 frontend/android/build-apk.ps1 create mode 100644 frontend/android/build.gradle create mode 100644 frontend/android/capacitor.settings.gradle create mode 100644 frontend/android/gradle.properties create mode 100644 frontend/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 frontend/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 frontend/android/gradlew create mode 100644 frontend/android/gradlew.bat create mode 100644 frontend/android/settings.gradle create mode 100644 frontend/android/variables.gradle create mode 100644 frontend/capacitor.config.ts create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.png create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/scripts/build-apk.ps1 create mode 100644 frontend/scripts/install-emulator.ps1 create mode 100644 frontend/scripts/update-app.ps1 create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api.ts create mode 100644 frontend/src/apiTypes.ts create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/auth.tsx create mode 100644 frontend/src/components/AppCalendar.tsx create mode 100644 frontend/src/components/WeightStepper.tsx create mode 100644 frontend/src/data/program.json create mode 100644 frontend/src/index.css create mode 100644 frontend/src/local/api.ts create mode 100644 frontend/src/local/db.ts create mode 100644 frontend/src/local/reschedule.ts create mode 100644 frontend/src/local/schedule.ts create mode 100644 frontend/src/local/types.ts create mode 100644 frontend/src/local/weights.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/HomePage.tsx create mode 100644 frontend/src/pages/LoginPage.tsx create mode 100644 frontend/src/pages/OnboardingPage.tsx create mode 100644 frontend/src/pages/SettingsPage.tsx create mode 100644 frontend/src/pages/WorkoutPage.tsx create mode 100644 frontend/src/vite-env.d.ts 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 Драфт.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d34299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +__pycache__/ +*.pyc +.venv/ +.env +node_modules/ +dist/ +.DS_Store +*.xlsx.bak +_draft_dump.json +frontend/android/.gradle/ +frontend/android/app/build/ +frontend/android/build/ +frontend/android/local.properties +*.apk +*.aab +*.keystore diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcc2a5e --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Benchpress Secret + +Мобильный web-интерфейс программы жима лёжа на 5 недель. + +## Стек + +- Frontend: React + Vite + TypeScript +- Backend: FastAPI +- DB: PostgreSQL +- Docker: `frontend` + `backend` + `db` + +## Запуск + +```bash +docker compose up --build +``` + +Открыть: http://localhost:3000 +API: http://localhost:8000/api/health + +## Локальная разработка без Docker UI + +```bash +docker compose up db backend --build +cd frontend +npm install +npm run dev +``` + +Vite proxy: `/api` → `http://localhost:8000` + +## Первый вход + +1. Задать пароль +2. Ввести 1ПМ (шаг 1.25 кг) +3. Выбрать 3 дня недели с перерывом ≥1 день +4. Выбрать дату старта + +## Настройки + +Импорт/экспорт JSON и сброс прогресса — в разделе «Настройки». diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..07f85aa --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,4 @@ +__pycache__ +*.pyc +.venv +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f63f44f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..421187c --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,24 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", extra="ignore") + + database_url: str = "postgresql+psycopg://benchpress:benchpress@localhost:5432/benchpress" + secret_key: str = "dev-secret-key-change-me" + access_token_expire_minutes: int = 60 * 24 * 30 + cors_origins: str = ( + "http://localhost:5173,http://localhost:3000,http://localhost," + "https://localhost,capacitor://localhost,http://10.0.2.2:8000" + ) + plate_step_kg: float = 1.25 + pause_limit_minutes: int = 15 + rest_min_days: int = 3 + rest_max_days: int = 5 + + @property + def cors_origin_list(self) -> list[str]: + return [o.strip() for o in self.cors_origins.split(",") if o.strip()] + + +settings = Settings() diff --git a/backend/app/data/program.json b/backend/app/data/program.json new file mode 100644 index 0000000..1f1ce1d --- /dev/null +++ b/backend/app/data/program.json @@ -0,0 +1,1689 @@ +{ + "weeks": [ + { + "week": 1, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 60.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 55.0, + "reps": 6 + }, + { + "percent": 45.0, + "reps": 8 + } + ] + }, + { + "name": "Молотковые сгибания", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + } + ] + }, + { + "name": "Разгибания с канатом", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + } + ] + }, + { + "week": 2, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + } + ] + }, + { + "name": "Вертикальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Обратная планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 20 + }, + { + "type": "reps", + "reps": 20 + }, + { + "type": "reps", + "reps": 20 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 6 + }, + { + "percent": 45.0, + "reps": 8 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + } + ] + }, + { + "week": 3, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 4 + }, + { + "percent": 80.0, + "reps": 4 + }, + { + "percent": 80.0, + "reps": 4 + } + ] + }, + { + "name": "Молотковые сгибания", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Разгибания с канатом", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + } + ] + }, + { + "name": "Вертикальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Горизонтальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Обратная планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 85.0, + "reps": 4 + }, + { + "percent": 85.0, + "reps": 4 + }, + { + "percent": 85.0, + "reps": 4 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 5 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + } + ] + }, + { + "week": 4, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 2 + }, + { + "percent": 80.0, + "reps": 5 + }, + { + "percent": 80.0, + "reps": 5 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 5 + } + ] + }, + { + "name": "Молотковые сгибания", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + } + ] + }, + { + "name": "Разгибания с канатом", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 6 + }, + { + "percent": 80.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 6 + }, + { + "percent": 45.0, + "reps": 8 + } + ] + }, + { + "name": "Вертикальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Обратная планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + } + ] + }, + { + "week": 5, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 5 + }, + { + "percent": 80.0, + "reps": 5 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 85.0, + "reps": 2 + }, + { + "percent": 90.0, + "reps": 4 + }, + { + "percent": 90.0, + "reps": 4 + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..7c7d5e1 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,21 @@ +from collections.abc import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker + +from app.config import settings + +engine = create_engine(settings.database_url, pool_pre_ping=True) +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + + +class Base(DeclarativeBase): + pass + + +def get_db() -> Generator[Session, None, None]: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..298f3ee --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings +from app.database import Base, engine +from app.routers import actions, auth, workouts + +app = FastAPI(title="Benchpress Secret", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origin_list, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth.router) +app.include_router(workouts.router) +app.include_router(actions.router) + + +@app.on_event("startup") +def on_startup() -> None: + Base.metadata.create_all(bind=engine) + + +@app.get("/api/health") +def health() -> dict[str, str]: + return {"status": "ok"} diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..8f828e5 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from datetime import date, datetime +from typing import Any, Optional + +from sqlalchemy import Boolean, Date, DateTime, Float, ForeignKey, Integer, String, Text, UniqueConstraint +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + +class Profile(Base): + __tablename__ = "profiles" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + one_rm_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + training_weekdays: Mapped[Optional[list[Any]]] = mapped_column(JSONB, nullable=True) + start_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True) + onboarded: Mapped[bool] = mapped_column(Boolean, default=False) + phase: Mapped[str] = mapped_column(String(32), default="active") + # active | rest | probe | completed_cycle + rest_until: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + probe_available_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + cycle_number: Mapped[int] = mapped_column(Integer, default=1) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +class Workout(Base): + __tablename__ = "workouts" + __table_args__ = (UniqueConstraint("scheduled_date", "cycle_number", name="uq_workout_date_cycle"),) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + cycle_number: Mapped[int] = mapped_column(Integer, default=1) + week_number: Mapped[int] = mapped_column(Integer, nullable=False) + day_index: Mapped[int] = mapped_column(Integer, nullable=False) # 0..2 within week + scheduled_date: Mapped[date] = mapped_column(Date, nullable=False, index=True) + status: Mapped[str] = mapped_column(String(32), default="planned") + # planned | in_progress | paused | completed | abandoned + started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + paused_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + current_exercise_index: Mapped[int] = mapped_column(Integer, default=0) + current_set_index: Mapped[int] = mapped_column(Integer, default=0) + rest_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + + exercises: Mapped[list[WorkoutExercise]] = relationship( + back_populates="workout", cascade="all, delete-orphan", order_by="WorkoutExercise.order_index" + ) + + +class WorkoutExercise(Base): + __tablename__ = "workout_exercises" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + workout_id: Mapped[int] = mapped_column(ForeignKey("workouts.id", ondelete="CASCADE"), index=True) + order_index: Mapped[int] = mapped_column(Integer, nullable=False) + name: Mapped[str] = mapped_column(String(128), nullable=False) + kind: Mapped[str] = mapped_column(String(32), nullable=False) # bench | accessory | failure + prescribed_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + weight_locked: Mapped[bool] = mapped_column(Boolean, default=False) + needs_weight_input: Mapped[bool] = mapped_column(Boolean, default=False) + pending_prompt: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) + # None | choose_weight_missed | choose_weight_easy | confirm_easy + + workout: Mapped[Workout] = relationship(back_populates="exercises") + sets: Mapped[list[WorkoutSet]] = relationship( + back_populates="exercise", cascade="all, delete-orphan", order_by="WorkoutSet.set_index" + ) + + +class WorkoutSet(Base): + __tablename__ = "workout_sets" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + exercise_id: Mapped[int] = mapped_column(ForeignKey("workout_exercises.id", ondelete="CASCADE"), index=True) + set_index: Mapped[int] = mapped_column(Integer, nullable=False) + target_percent: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + target_reps: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + target_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + is_failure: Mapped[bool] = mapped_column(Boolean, default=False) + completed: Mapped[bool] = mapped_column(Boolean, default=False) + actual_reps: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + actual_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + + exercise: Mapped[WorkoutExercise] = relationship(back_populates="sets") + + +class AccessoryState(Base): + """Tracks working weight and lock status for accessory exercises within a cycle.""" + + __tablename__ = "accessory_states" + __table_args__ = (UniqueConstraint("cycle_number", "exercise_name", name="uq_accessory_cycle_name"),) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + cycle_number: Mapped[int] = mapped_column(Integer, default=1) + exercise_name: Mapped[str] = mapped_column(String(128), nullable=False) + weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + locked: Mapped[bool] = mapped_column(Boolean, default=False) + pending_weight_kg: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +class AppMeta(Base): + __tablename__ = "app_meta" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + key: Mapped[str] = mapped_column(String(64), unique=True) + value: Mapped[str] = mapped_column(Text, nullable=False) diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routers/actions.py b/backend/app/routers/actions.py new file mode 100644 index 0000000..26f3d76 --- /dev/null +++ b/backend/app/routers/actions.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from datetime import date, timedelta + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models import User, Workout +from app.routers.auth import require_auth +from app.schemas import ( + AdjustSetRequest, + CompleteSetRequest, + MoveWorkoutRequest, + PromptChoiceRequest, + SetWeightRequest, + WorkoutDetail, +) +from app.services.auth import get_or_create_profile +from app.services.reschedule import plan_move +from app.services.workout import ( + adjust_completed_set, + complete_set, + load_workout, + pause_workout, + resolve_prompt, + resume_workout, + set_exercise_weight, + start_workout, + workout_detail, +) + +router = APIRouter(prefix="/api/workouts", tags=["workout-actions"]) + + +@router.post("/{workout_id}/start", response_model=WorkoutDetail) +def api_start(workout_id: int, _: User = Depends(require_auth), db: Session = Depends(get_db)) -> WorkoutDetail: + w = start_workout(db, load_workout(db, workout_id)) + return workout_detail(db, w) + + +@router.post("/{workout_id}/pause", response_model=WorkoutDetail) +def api_pause(workout_id: int, _: User = Depends(require_auth), db: Session = Depends(get_db)) -> WorkoutDetail: + w = pause_workout(db, load_workout(db, workout_id)) + return workout_detail(db, w) + + +@router.post("/{workout_id}/resume", response_model=WorkoutDetail) +def api_resume(workout_id: int, _: User = Depends(require_auth), db: Session = Depends(get_db)) -> WorkoutDetail: + w = resume_workout(db, load_workout(db, workout_id)) + return workout_detail(db, w) + + +@router.post("/{workout_id}/complete-set", response_model=WorkoutDetail) +def api_complete_set( + workout_id: int, + body: CompleteSetRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> WorkoutDetail: + w = complete_set( + db, + load_workout(db, workout_id), + actual_reps=body.actual_reps, + actual_weight_kg=body.actual_weight_kg, + ) + return workout_detail(db, w) + + +@router.post("/{workout_id}/exercises/{exercise_id}/weight", response_model=WorkoutDetail) +def api_set_weight( + workout_id: int, + exercise_id: int, + body: SetWeightRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> WorkoutDetail: + workout = load_workout(db, workout_id) + exercise = next((e for e in workout.exercises if e.id == exercise_id), None) + if not exercise: + raise HTTPException(404, "Упражнение не найдено") + w = set_exercise_weight(db, workout, exercise, body.weight_kg) + return workout_detail(db, w) + + +@router.post("/{workout_id}/exercises/{exercise_id}/sets/{set_id}", response_model=WorkoutDetail) +def api_adjust_set( + workout_id: int, + exercise_id: int, + set_id: int, + body: AdjustSetRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> WorkoutDetail: + workout = load_workout(db, workout_id) + exercise = next((e for e in workout.exercises if e.id == exercise_id), None) + if not exercise: + raise HTTPException(404, "Упражнение не найдено") + workout_set = next((s for s in exercise.sets if s.id == set_id), None) + if not workout_set: + raise HTTPException(404, "Подход не найден") + w = adjust_completed_set( + db, + workout, + exercise, + workout_set, + actual_weight_kg=body.actual_weight_kg, + actual_reps=body.actual_reps, + ) + return workout_detail(db, w) + + +@router.post("/{workout_id}/move", response_model=WorkoutDetail) +def api_move( + workout_id: int, + body: MoveWorkoutRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> WorkoutDetail: + workout = load_workout(db, workout_id) + profile = get_or_create_profile(db) + weekdays = profile.training_weekdays or [] + if len(weekdays) != 3: + raise HTTPException(400, "Не заданы дни тренировок") + rows = db.query(Workout).filter(Workout.cycle_number == workout.cycle_number).all() + assigned = plan_move(rows, workout_id, body.scheduled_date, list(weekdays)) + for row in rows: + if row.id in assigned: + row.scheduled_date = date(2099, 1, 1) + timedelta(days=row.id % 300) + db.flush() + by_id = {row.id: row for row in rows} + for workout_row_id, scheduled in assigned.items(): + by_id[workout_row_id].scheduled_date = scheduled + db.commit() + return workout_detail(db, load_workout(db, workout_id)) + + +@router.post("/{workout_id}/exercises/{exercise_id}/prompt", response_model=WorkoutDetail) +def api_prompt( + workout_id: int, + exercise_id: int, + body: PromptChoiceRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> WorkoutDetail: + workout = load_workout(db, workout_id) + exercise = next((e for e in workout.exercises if e.id == exercise_id), None) + if not exercise: + raise HTTPException(404, "Упражнение не найдено") + w = resolve_prompt( + db, + workout, + exercise, + choice=body.choice, + weight_kg=body.weight_kg, + increase_kg=body.increase_kg, + decrease_kg=body.decrease_kg, + ) + return workout_detail(db, w) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..c305296 --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models import User +from app.schemas import LoginRequest, SetupPasswordRequest, StatusResponse, TokenResponse +from app.services.auth import ( + create_access_token, + decode_token, + get_or_create_profile, + get_user, + has_password, + hash_password, + verify_password, +) + +router = APIRouter(prefix="/api/auth", tags=["auth"]) +security = HTTPBearer(auto_error=False) + + +def require_auth( + creds: Optional[HTTPAuthorizationCredentials] = Depends(security), + db: Session = Depends(get_db), +) -> User: + if not creds: + raise HTTPException(401, "Требуется авторизация") + sub = decode_token(creds.credentials) + if not sub: + raise HTTPException(401, "Недействительный токен") + user = get_user(db) + if not user: + raise HTTPException(401, "Пользователь не найден") + return user + + +@router.get("/status", response_model=StatusResponse) +def auth_status(db: Session = Depends(get_db)) -> StatusResponse: + profile = get_or_create_profile(db) + return StatusResponse( + has_password=has_password(db), + onboarded=profile.onboarded, + phase=profile.phase, + ) + + +@router.post("/setup", response_model=TokenResponse) +def setup_password(body: SetupPasswordRequest, db: Session = Depends(get_db)) -> TokenResponse: + if has_password(db): + raise HTTPException(400, "Пароль уже задан") + user = User(password_hash=hash_password(body.password), created_at=datetime.utcnow()) + db.add(user) + get_or_create_profile(db) + db.commit() + profile = get_or_create_profile(db) + return TokenResponse( + access_token=create_access_token(), + needs_setup=False, + onboarded=profile.onboarded, + ) + + +@router.post("/login", response_model=TokenResponse) +def login(body: LoginRequest, db: Session = Depends(get_db)) -> TokenResponse: + user = get_user(db) + if not user: + raise HTTPException(400, "Сначала задайте пароль") + if not verify_password(body.password, user.password_hash): + raise HTTPException(401, "Неверный пароль") + profile = get_or_create_profile(db) + return TokenResponse( + access_token=create_access_token(), + onboarded=profile.onboarded, + ) diff --git a/backend/app/routers/workouts.py b/backend/app/routers/workouts.py new file mode 100644 index 0000000..958517f --- /dev/null +++ b/backend/app/routers/workouts.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +from datetime import date, datetime, timedelta + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, joinedload + +from app.database import get_db +from app.models import AccessoryState, Profile, User, Workout, WorkoutExercise +from app.routers.auth import require_auth +from app.schemas import ( + CalendarResponse, + ExportResponse, + ImportRequest, + MessageResponse, + OnboardingRequest, + ProbeRequest, + ProfileResponse, + StartRestRequest, + TrainingDaysRequest, + WorkoutDetail, + WorkoutSummary, +) +from app.services.reschedule import plan_weekday_change +from app.services.auth import get_or_create_profile +from app.services.schedule import clear_cycle_workouts, generate_schedule +from app.services.workout import ( + load_workout, + profile_response, + start_rest_phase, + submit_probe, + workout_detail, + workout_summary, +) +from app.services.weights import round_to_plate + +router = APIRouter(prefix="/api", tags=["app"]) + + +@router.get("/profile", response_model=ProfileResponse) +def get_profile(_: User = Depends(require_auth), db: Session = Depends(get_db)) -> ProfileResponse: + return profile_response(get_or_create_profile(db)) + + +@router.post("/onboarding", response_model=ProfileResponse) +def onboarding( + body: OnboardingRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> ProfileResponse: + profile = get_or_create_profile(db) + if profile.onboarded: + raise HTTPException(400, "Онбординг уже пройден. Используйте сброс в настройках.") + + one_rm = round_to_plate(body.one_rm_kg) + profile.one_rm_kg = one_rm + profile.training_weekdays = body.training_weekdays + profile.start_date = body.start_date + profile.onboarded = True + profile.phase = "active" + profile.cycle_number = 1 + + clear_cycle_workouts(db, 1) + generate_schedule( + db, + one_rm_kg=one_rm, + training_weekdays=body.training_weekdays, + start_date=body.start_date, + cycle_number=1, + ) + db.commit() + db.refresh(profile) + return profile_response(profile) + + +@router.get("/calendar", response_model=CalendarResponse) +def calendar( + year: int, + month: int, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> CalendarResponse: + if month < 1 or month > 12: + raise HTTPException(400, "Неверный месяц") + profile = get_or_create_profile(db) + workouts = ( + db.query(Workout) + .options(joinedload(Workout.exercises)) + .filter( + Workout.cycle_number == profile.cycle_number, + ) + .order_by(Workout.scheduled_date) + .all() + ) + # filter by month for display but also include all cycle for week strip — return month filtered + month_workouts = [w for w in workouts if w.scheduled_date.year == year and w.scheduled_date.month == month] + return CalendarResponse( + year=year, + month=month, + workouts=[workout_summary(w) for w in month_workouts], + profile=profile_response(profile), + ) + + +@router.post("/training-days", response_model=ProfileResponse) +def update_training_days( + body: TrainingDaysRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> ProfileResponse: + profile = get_or_create_profile(db) + if not profile.onboarded: + raise HTTPException(400, "Сначала создайте программу") + rows = db.query(Workout).filter(Workout.cycle_number == profile.cycle_number).all() + done = [w.scheduled_date for w in rows if w.status == "completed"] + start = date.today() + if done: + start = max(start, max(done) + timedelta(days=1)) + assigned = plan_weekday_change(rows, list(body.training_weekdays), start) + for row in rows: + if row.id in assigned: + row.scheduled_date = date(2099, 1, 1) + timedelta(days=row.id % 300) + db.flush() + by_id = {row.id: row for row in rows} + for workout_row_id, scheduled in assigned.items(): + by_id[workout_row_id].scheduled_date = scheduled + profile.training_weekdays = list(body.training_weekdays) + db.commit() + db.refresh(profile) + return profile_response(profile) + + +@router.get("/workouts/upcoming", response_model=list[WorkoutSummary]) +def upcoming( + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> list[WorkoutSummary]: + profile = get_or_create_profile(db) + workouts = ( + db.query(Workout) + .options(joinedload(Workout.exercises)) + .filter(Workout.cycle_number == profile.cycle_number) + .order_by(Workout.scheduled_date) + .all() + ) + return [workout_summary(w) for w in workouts] + + +@router.get("/workouts/{workout_id}", response_model=WorkoutDetail) +def get_workout( + workout_id: int, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> WorkoutDetail: + return workout_detail(db, load_workout(db, workout_id)) + + +@router.post("/rest/start", response_model=ProfileResponse) +def rest_start( + body: StartRestRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> ProfileResponse: + profile = get_or_create_profile(db) + return profile_response(start_rest_phase(db, profile, body.rest_days)) + + +@router.post("/probe", response_model=ProfileResponse) +def probe( + body: ProbeRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> ProfileResponse: + profile = get_or_create_profile(db) + return profile_response( + submit_probe( + db, + profile, + one_rm_kg=body.one_rm_kg, + start_date=body.start_date, + training_weekdays=body.training_weekdays, + ) + ) + + +@router.get("/export", response_model=ExportResponse) +def export_data(_: User = Depends(require_auth), db: Session = Depends(get_db)) -> ExportResponse: + profile = get_or_create_profile(db) + workouts = ( + db.query(Workout) + .options(joinedload(Workout.exercises).joinedload(WorkoutExercise.sets)) + .order_by(Workout.scheduled_date) + .all() + ) + accessories = db.query(AccessoryState).all() + + def dump_workout(w: Workout) -> dict: + return { + "cycle_number": w.cycle_number, + "week_number": w.week_number, + "day_index": w.day_index, + "scheduled_date": w.scheduled_date.isoformat(), + "status": w.status, + "started_at": w.started_at.isoformat() if w.started_at else None, + "completed_at": w.completed_at.isoformat() if w.completed_at else None, + "current_exercise_index": w.current_exercise_index, + "current_set_index": w.current_set_index, + "exercises": [ + { + "order_index": e.order_index, + "name": e.name, + "kind": e.kind, + "prescribed_weight_kg": e.prescribed_weight_kg, + "weight_locked": e.weight_locked, + "needs_weight_input": e.needs_weight_input, + "pending_prompt": e.pending_prompt, + "sets": [ + { + "set_index": s.set_index, + "target_percent": s.target_percent, + "target_reps": s.target_reps, + "target_weight_kg": s.target_weight_kg, + "is_failure": s.is_failure, + "completed": s.completed, + "actual_reps": s.actual_reps, + "actual_weight_kg": s.actual_weight_kg, + "completed_at": s.completed_at.isoformat() if s.completed_at else None, + } + for s in sorted(e.sets, key=lambda x: x.set_index) + ], + } + for e in sorted(w.exercises, key=lambda x: x.order_index) + ], + } + + return ExportResponse( + exported_at=datetime.utcnow(), + data={ + "profile": { + "one_rm_kg": profile.one_rm_kg, + "training_weekdays": profile.training_weekdays, + "start_date": profile.start_date.isoformat() if profile.start_date else None, + "onboarded": profile.onboarded, + "phase": profile.phase, + "rest_until": profile.rest_until.isoformat() if profile.rest_until else None, + "probe_available_at": profile.probe_available_at.isoformat() + if profile.probe_available_at + else None, + "cycle_number": profile.cycle_number, + }, + "workouts": [dump_workout(w) for w in workouts], + "accessories": [ + { + "cycle_number": a.cycle_number, + "exercise_name": a.exercise_name, + "weight_kg": a.weight_kg, + "locked": a.locked, + "pending_weight_kg": a.pending_weight_kg, + } + for a in accessories + ], + }, + ) + + +@router.post("/import", response_model=MessageResponse) +def import_data( + body: ImportRequest, + _: User = Depends(require_auth), + db: Session = Depends(get_db), +) -> MessageResponse: + if body.version != 1: + raise HTTPException(400, "Неподдерживаемая версия экспорта") + data = body.data + # full replace of progress + db.query(Workout).delete() + db.query(AccessoryState).delete() + profile = get_or_create_profile(db) + p = data.get("profile", {}) + profile.one_rm_kg = p.get("one_rm_kg") + profile.training_weekdays = p.get("training_weekdays") + profile.start_date = datetime.fromisoformat(p["start_date"]).date() if p.get("start_date") else None + profile.onboarded = bool(p.get("onboarded")) + profile.phase = p.get("phase") or "active" + profile.rest_until = datetime.fromisoformat(p["rest_until"]) if p.get("rest_until") else None + profile.probe_available_at = ( + datetime.fromisoformat(p["probe_available_at"]) if p.get("probe_available_at") else None + ) + profile.cycle_number = int(p.get("cycle_number") or 1) + + from app.models import WorkoutSet + + for a in data.get("accessories", []): + db.add( + AccessoryState( + cycle_number=a["cycle_number"], + exercise_name=a["exercise_name"], + weight_kg=a.get("weight_kg"), + locked=bool(a.get("locked")), + pending_weight_kg=a.get("pending_weight_kg"), + ) + ) + + for w in data.get("workouts", []): + workout = Workout( + cycle_number=w["cycle_number"], + week_number=w["week_number"], + day_index=w["day_index"], + scheduled_date=datetime.fromisoformat(w["scheduled_date"]).date(), + status=w.get("status") or "planned", + started_at=datetime.fromisoformat(w["started_at"]) if w.get("started_at") else None, + completed_at=datetime.fromisoformat(w["completed_at"]) if w.get("completed_at") else None, + current_exercise_index=w.get("current_exercise_index") or 0, + current_set_index=w.get("current_set_index") or 0, + ) + db.add(workout) + db.flush() + for e in w.get("exercises", []): + ex = WorkoutExercise( + workout_id=workout.id, + order_index=e["order_index"], + name=e["name"], + kind=e["kind"], + prescribed_weight_kg=e.get("prescribed_weight_kg"), + weight_locked=bool(e.get("weight_locked")), + needs_weight_input=bool(e.get("needs_weight_input")), + pending_prompt=e.get("pending_prompt"), + ) + db.add(ex) + db.flush() + for s in e.get("sets", []): + db.add( + WorkoutSet( + exercise_id=ex.id, + set_index=s["set_index"], + target_percent=s.get("target_percent"), + target_reps=s.get("target_reps"), + target_weight_kg=s.get("target_weight_kg"), + is_failure=bool(s.get("is_failure")), + completed=bool(s.get("completed")), + actual_reps=s.get("actual_reps"), + actual_weight_kg=s.get("actual_weight_kg"), + completed_at=datetime.fromisoformat(s["completed_at"]) + if s.get("completed_at") + else None, + ) + ) + db.commit() + return MessageResponse(message="Данные импортированы") + + +@router.post("/reset", response_model=MessageResponse) +def reset_all(_: User = Depends(require_auth), db: Session = Depends(get_db)) -> MessageResponse: + db.query(Workout).delete() + db.query(AccessoryState).delete() + profile = get_or_create_profile(db) + profile.one_rm_kg = None + profile.training_weekdays = None + profile.start_date = None + profile.onboarded = False + profile.phase = "active" + profile.rest_until = None + profile.probe_available_at = None + profile.cycle_number = 1 + db.commit() + return MessageResponse(message="Прогресс удалён. Пройдите онбординг заново.") diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..ca7ad9b --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +from datetime import date, datetime +from typing import Any, Literal, Optional + +from pydantic import BaseModel, Field, field_validator + + +Weekday = Literal[0, 1, 2, 3, 4, 5, 6] # Mon=0 .. Sun=6 + + +class SetupPasswordRequest(BaseModel): + password: str = Field(min_length=4, max_length=128) + + +class LoginRequest(BaseModel): + password: str + + +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" + needs_setup: bool = False + onboarded: bool = False + + +class StatusResponse(BaseModel): + has_password: bool + onboarded: bool + phase: str = "active" + + +class OnboardingRequest(BaseModel): + one_rm_kg: float = Field(gt=0) + training_weekdays: list[int] = Field(min_length=3, max_length=3) + start_date: date + + @field_validator("training_weekdays") + @classmethod + def validate_days(cls, v: list[int]) -> list[int]: + if len(set(v)) != 3: + raise ValueError("Нужно выбрать 3 разных дня") + for d in v: + if d < 0 or d > 6: + raise ValueError("День недели должен быть 0–6") + sorted_days = sorted(v) + # check circular gap of at least 1 day between consecutive selected days + for i in range(3): + a = sorted_days[i] + b = sorted_days[(i + 1) % 3] + if i < 2: + gap = b - a - 1 + else: + gap = (a + 7) - b - 1 + if gap < 1: + raise ValueError("Между выбранными днями нужен минимум 1 день перерыва") + return sorted(v) + + +class ProfileResponse(BaseModel): + one_rm_kg: Optional[float] + training_weekdays: Optional[list[int]] + start_date: Optional[date] + onboarded: bool + phase: str + rest_until: Optional[datetime] + probe_available_at: Optional[datetime] + cycle_number: int + rest_countdown_seconds: Optional[int] = None + can_probe: bool = False + + +class SetResponse(BaseModel): + id: int + set_index: int + target_percent: Optional[float] + target_reps: Optional[int] + target_weight_kg: Optional[float] + is_failure: bool + completed: bool + actual_reps: Optional[int] + actual_weight_kg: Optional[float] + + +class ExerciseResponse(BaseModel): + id: int + order_index: int + name: str + kind: str + prescribed_weight_kg: Optional[float] + weight_locked: bool + needs_weight_input: bool + pending_prompt: Optional[str] + weight_step_kg: float = 1.25 + sets: list[SetResponse] + + +class WorkoutSummary(BaseModel): + id: int + cycle_number: int + week_number: int + day_index: int + scheduled_date: date + status: str + exercise_count: int = 0 + + +class WorkoutDetail(BaseModel): + id: int + cycle_number: int + week_number: int + day_index: int + scheduled_date: date + status: str + started_at: Optional[datetime] + paused_at: Optional[datetime] + completed_at: Optional[datetime] + current_exercise_index: int + current_set_index: int + rest_started_at: Optional[datetime] + rest_elapsed_seconds: int = 0 + can_start: bool = False + can_resume: bool = False + pause_expires_at: Optional[datetime] = None + exercises: list[ExerciseResponse] + + +class CalendarResponse(BaseModel): + year: int + month: int + workouts: list[WorkoutSummary] + profile: ProfileResponse + + +class CompleteSetRequest(BaseModel): + actual_reps: Optional[int] = None + actual_weight_kg: Optional[float] = None + + +class SetWeightRequest(BaseModel): + weight_kg: float = Field(gt=0) + + +class AdjustSetRequest(BaseModel): + actual_weight_kg: float = Field(gt=0) + actual_reps: int = Field(ge=0) + + +class PromptChoiceRequest(BaseModel): + choice: Literal["easy_yes", "easy_no"] + weight_kg: Optional[float] = None + increase_kg: Optional[float] = None + decrease_kg: Optional[float] = None + + +class StartRestRequest(BaseModel): + rest_days: int = Field(ge=3, le=5) + + +class ProbeRequest(BaseModel): + one_rm_kg: float = Field(gt=0) + start_date: date + training_weekdays: Optional[list[int]] = None + + +class ExportResponse(BaseModel): + version: int = 1 + exported_at: datetime + data: dict[str, Any] + + +class ImportRequest(BaseModel): + version: int + data: dict[str, Any] + + +class MessageResponse(BaseModel): + message: str + workout: Optional[WorkoutDetail] = None + profile: Optional[ProfileResponse] = None + + +class MoveWorkoutRequest(BaseModel): + scheduled_date: date + + +class TrainingDaysRequest(BaseModel): + training_weekdays: list[int] = Field(min_length=3, max_length=3) + + @field_validator("training_weekdays") + @classmethod + def validate_days(cls, v: list[int]) -> list[int]: + return OnboardingRequest.validate_days(v) 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/auth.py b/backend/app/services/auth.py new file mode 100644 index 0000000..d5b2112 --- /dev/null +++ b/backend/app/services/auth.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from datetime import date, datetime, timedelta +from typing import Optional + +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy.orm import Session + +from app.config import settings +from app.models import Profile, User + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(password: str, password_hash: str) -> bool: + return pwd_context.verify(password, password_hash) + + +def create_access_token(subject: str = "user") -> str: + expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) + return jwt.encode( + {"sub": subject, "exp": expire}, + settings.secret_key, + algorithm="HS256", + ) + + +def decode_token(token: str) -> Optional[str]: + try: + payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"]) + return payload.get("sub") + except JWTError: + return None + + +def get_or_create_profile(db: Session) -> Profile: + profile = db.query(Profile).first() + if not profile: + profile = Profile() + db.add(profile) + db.commit() + db.refresh(profile) + return profile + + +def has_password(db: Session) -> bool: + return db.query(User).first() is not None + + +def get_user(db: Session) -> Optional[User]: + return db.query(User).first() diff --git a/backend/app/services/program.py b/backend/app/services/program.py new file mode 100644 index 0000000..85be848 --- /dev/null +++ b/backend/app/services/program.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import json +from functools import lru_cache +from pathlib import Path +from typing import Any + + +PROGRAM_PATH = Path(__file__).resolve().parent.parent / "data" / "program.json" + + +@lru_cache(maxsize=1) +def load_program() -> dict[str, Any]: + with PROGRAM_PATH.open(encoding="utf-8") as f: + return json.load(f) + + +def get_week(week_number: int) -> dict[str, Any]: + program = load_program() + for week in program["weeks"]: + if week["week"] == week_number: + return week + raise ValueError(f"Неделя {week_number} не найдена") diff --git a/backend/app/services/reschedule.py b/backend/app/services/reschedule.py new file mode 100644 index 0000000..86297c7 --- /dev/null +++ b/backend/app/services/reschedule.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from datetime import date, timedelta + +from fastapi import HTTPException + + +def _add(d: date, days: int) -> date: + return d + timedelta(days=days) + + +def _step(d: date, weekdays: list[int], direction: int) -> date: + selected = set(weekdays) + cursor = _add(d, direction) + for _ in range(28): + if cursor.weekday() in selected: + return cursor + cursor = _add(cursor, direction) + raise HTTPException(400, "Не удалось найти тренировочный день") + + +def _on_or_after(d: date, weekdays: list[int]) -> date: + if d.weekday() in set(weekdays): + return d + return _step(_add(d, -1), weekdays, 1) + + +def _order(week: int, day_index: int) -> int: + return week * 10 + day_index + + +def _next_training(after: date, weekdays: list[int], taken: set[date]) -> date: + value = _step(after, weekdays, 1) + while value in taken: + value = _step(value, weekdays, 1) + return value + + +def _place(desired: date, previous: date | None, weekdays: list[int], taken: set[date]) -> date: + value = desired + if previous is not None and value <= previous: + value = _next_training(previous, weekdays, taken) + while value in taken: + value = _next_training(value, weekdays, taken) + return value + + +def plan_move(workouts: list, moved_id: int, new_date: date, weekdays: list[int]) -> dict[int, date]: + moved = next((w for w in workouts if w.id == moved_id), None) + if moved is None: + raise HTTPException(404, "Тренировка не найдена") + if moved.status == "completed": + raise HTTPException(400, "Выполненную тренировку переносить нельзя") + if new_date < date.today(): + raise HTTPException(400, "В закрытый период переносить нельзя") + + sorted_items = sorted(workouts, key=lambda w: (_order(w.week_number, w.day_index), w.id)) + completed = [w for w in sorted_items if w.status == "completed"] + if any(w.scheduled_date == new_date for w in completed): + raise HTTPException(400, "На эту дату уже есть выполненная тренировка") + + moved_order = _order(moved.week_number, moved.day_index) + earlier = [w for w in completed if _order(w.week_number, w.day_index) < moved_order] + later = [w for w in completed if _order(w.week_number, w.day_index) > moved_order] + if earlier and new_date <= earlier[-1].scheduled_date: + raise HTTPException(400, "Дата должна быть позже уже выполненной тренировки") + if later and new_date >= later[0].scheduled_date: + raise HTTPException(400, "Дата должна быть раньше уже выполненной следующей тренировки") + + taken = {w.scheduled_date for w in completed} + assigned: dict[int, date] = {} + index = 0 + while index < len(sorted_items): + if sorted_items[index].status == "completed": + index += 1 + continue + start = index + while index < len(sorted_items) and sorted_items[index].status != "completed": + index += 1 + segment = sorted_items[start:index] + prev_anchor = sorted_items[start - 1] if start > 0 else None + next_anchor = sorted_items[index] if index < len(sorted_items) else None + proposed = sorted(new_date if item.id == moved_id else item.scheduled_date for item in segment) + dates: list[date] = [] + for raw in proposed: + value = _place(raw, dates[-1] if dates else None, weekdays, taken) + if prev_anchor is not None and value <= prev_anchor.scheduled_date: + raise HTTPException(400, "Дата должна быть позже уже выполненной тренировки") + if next_anchor is not None and value >= next_anchor.scheduled_date: + raise HTTPException(400, "Дата должна быть раньше уже выполненной следующей тренировки") + dates.append(value) + taken.add(value) + for item, value in zip(segment, dates): + assigned[item.id] = value + + return assigned + + +def plan_weekday_change(workouts: list, weekdays: list[int], start_from: date) -> dict[int, date]: + open_items = sorted( + (w for w in workouts if w.status != "completed"), + key=lambda w: _order(w.week_number, w.day_index), + ) + taken = {w.scheduled_date for w in workouts if w.status == "completed"} + assigned: dict[int, date] = {} + cursor = start_from + for item in open_items: + date_value = _on_or_after(cursor, weekdays) + while date_value in taken: + date_value = _step(date_value, weekdays, 1) + assigned[item.id] = date_value + taken.add(date_value) + cursor = _add(date_value, 1) + return assigned diff --git a/backend/app/services/schedule.py b/backend/app/services/schedule.py new file mode 100644 index 0000000..de88401 --- /dev/null +++ b/backend/app/services/schedule.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from datetime import date, timedelta + +from sqlalchemy.orm import Session + +from app.models import AccessoryState, Workout, WorkoutExercise, WorkoutSet +from app.services.program import load_program +from app.services.weights import bench_weight + + +WEEKDAY_NAMES = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] + + +def next_dates_for_weekdays(start: date, weekdays: list[int], count: int) -> list[date]: + """Return `count` dates on selected weekdays starting from start (inclusive if matches).""" + selected = set(weekdays) + dates: list[date] = [] + cursor = start + # safety limit + for _ in range(400): + if cursor.weekday() in selected: + dates.append(cursor) + if len(dates) >= count: + break + cursor += timedelta(days=1) + if len(dates) < count: + raise ValueError("Не удалось построить расписание") + return dates + + +def clear_cycle_workouts(db: Session, cycle_number: int) -> None: + workouts = db.query(Workout).filter(Workout.cycle_number == cycle_number).all() + for w in workouts: + db.delete(w) + db.query(AccessoryState).filter(AccessoryState.cycle_number == cycle_number).delete() + db.flush() + + +def generate_schedule( + db: Session, + *, + one_rm_kg: float, + training_weekdays: list[int], + start_date: date, + cycle_number: int, +) -> list[Workout]: + program = load_program() + total_days = sum(len(w["days"]) for w in program["weeks"]) + dates = next_dates_for_weekdays(start_date, training_weekdays, total_days) + + # Map chronological day_index within week order from template (day 0,1,2 of each week) + date_idx = 0 + created: list[Workout] = [] + + for week in program["weeks"]: + for day_index, day in enumerate(week["days"]): + scheduled = dates[date_idx] + date_idx += 1 + workout = Workout( + cycle_number=cycle_number, + week_number=week["week"], + day_index=day_index, + scheduled_date=scheduled, + status="planned", + ) + db.add(workout) + db.flush() + + for order_index, ex in enumerate(day["exercises"]): + kind = ex["kind"] + accessory_state = None + needs_weight = False + prescribed = None + locked = False + + if kind == "accessory": + accessory_state = ( + db.query(AccessoryState) + .filter( + AccessoryState.cycle_number == cycle_number, + AccessoryState.exercise_name == ex["name"], + ) + .first() + ) + if accessory_state and accessory_state.locked and accessory_state.weight_kg is not None: + prescribed = accessory_state.weight_kg + locked = True + elif accessory_state and accessory_state.pending_weight_kg is not None: + prescribed = accessory_state.pending_weight_kg + needs_weight = False + elif accessory_state and accessory_state.weight_kg is not None and not accessory_state.locked: + prescribed = accessory_state.weight_kg + needs_weight = False + else: + needs_weight = True + + we = WorkoutExercise( + workout_id=workout.id, + order_index=order_index, + name=ex["name"], + kind=kind, + prescribed_weight_kg=prescribed, + weight_locked=locked, + needs_weight_input=needs_weight, + ) + db.add(we) + db.flush() + + for set_index, s in enumerate(ex["sets"]): + if kind == "bench": + tw = bench_weight(one_rm_kg, s["percent"]) + ws = WorkoutSet( + exercise_id=we.id, + set_index=set_index, + target_percent=s["percent"], + target_reps=s["reps"], + target_weight_kg=tw, + is_failure=False, + ) + elif kind == "failure": + ws = WorkoutSet( + exercise_id=we.id, + set_index=set_index, + target_reps=None, + target_weight_kg=None, + is_failure=True, + ) + else: + reps = s.get("reps") if isinstance(s, dict) else None + ws = WorkoutSet( + exercise_id=we.id, + set_index=set_index, + target_reps=reps, + target_weight_kg=prescribed, + is_failure=False, + ) + db.add(ws) + + created.append(workout) + + db.flush() + return created + + +def apply_accessory_weight_to_future( + db: Session, + *, + cycle_number: int, + exercise_name: str, + weight_kg: float, + from_date: date, + lock: bool, +) -> None: + state = ( + db.query(AccessoryState) + .filter( + AccessoryState.cycle_number == cycle_number, + AccessoryState.exercise_name == exercise_name, + ) + .first() + ) + if not state: + state = AccessoryState(cycle_number=cycle_number, exercise_name=exercise_name) + db.add(state) + + if lock: + state.weight_kg = weight_kg + state.locked = True + state.pending_weight_kg = None + else: + state.pending_weight_kg = weight_kg + state.weight_kg = weight_kg + state.locked = False + + # Update future workouts' exercises of same name + future = ( + db.query(WorkoutExercise) + .join(Workout) + .filter( + Workout.cycle_number == cycle_number, + Workout.scheduled_date >= from_date, + Workout.status != "completed", + WorkoutExercise.name == exercise_name, + WorkoutExercise.kind == "accessory", + ) + .all() + ) + for we in future: + if we.sets and all(s.completed for s in we.sets): + continue + we.prescribed_weight_kg = weight_kg + we.weight_locked = lock + we.needs_weight_input = False + for s in we.sets: + if not s.completed: + s.target_weight_kg = weight_kg diff --git a/backend/app/services/weights.py b/backend/app/services/weights.py new file mode 100644 index 0000000..4b1a27f --- /dev/null +++ b/backend/app/services/weights.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from app.config import settings + +# Шаг корректировки/ввода веса вспомогательных (прибавка и снижение). +ACCESSORY_STEP_KG = 0.5 + + +def weight_step_for(exercise_name: str | None = None, *, kind: str | None = None) -> float: + """Жим — блины (config). Вспомогательные — 0.5 кг.""" + if kind == "bench" or (exercise_name == "Жим"): + return settings.plate_step_kg + if kind in ("accessory", "failure"): + return ACCESSORY_STEP_KG + if exercise_name: + return ACCESSORY_STEP_KG + return settings.plate_step_kg + + +def round_to_plate(weight_kg: float, step: float | None = None) -> float: + step = step if step is not None else settings.plate_step_kg + if weight_kg <= 0: + return 0.0 + return round(round(weight_kg / step) * step, 2) + + +def round_exercise_weight(weight_kg: float, exercise_name: str, *, kind: str | None = None) -> float: + return round_to_plate(weight_kg, weight_step_for(exercise_name, kind=kind)) + + +def bench_weight(one_rm_kg: float, percent: float) -> float: + """Жим: округление как раньше — по plate_step_kg (1.25).""" + return round_to_plate(one_rm_kg * (percent / 100.0), settings.plate_step_kg) diff --git a/backend/app/services/workout.py b/backend/app/services/workout.py new file mode 100644 index 0000000..b9900db --- /dev/null +++ b/backend/app/services/workout.py @@ -0,0 +1,589 @@ +from __future__ import annotations + +from datetime import date, datetime, timedelta +from typing import Optional + +from fastapi import HTTPException +from sqlalchemy.orm import Session, joinedload + +from app.config import settings +from app.models import AccessoryState, Profile, Workout, WorkoutExercise, WorkoutSet +from app.schemas import ( + ExerciseResponse, + ProfileResponse, + SetResponse, + WorkoutDetail, + WorkoutSummary, +) +from app.services.schedule import apply_accessory_weight_to_future, generate_schedule +from app.services.weights import round_exercise_weight, round_to_plate, weight_step_for + + + +def profile_response(profile: Profile, now: Optional[datetime] = None) -> ProfileResponse: + now = now or datetime.utcnow() + rest_countdown = None + can_probe = False + if profile.phase == "rest" and profile.probe_available_at: + delta = (profile.probe_available_at - now).total_seconds() + if delta > 0: + rest_countdown = int(delta) + else: + can_probe = True + elif profile.phase == "probe": + can_probe = True + return ProfileResponse( + one_rm_kg=profile.one_rm_kg, + training_weekdays=profile.training_weekdays, + start_date=profile.start_date, + onboarded=profile.onboarded, + phase=profile.phase if not (profile.phase == "rest" and can_probe) else "probe", + rest_until=profile.rest_until, + probe_available_at=profile.probe_available_at, + cycle_number=profile.cycle_number, + rest_countdown_seconds=rest_countdown, + can_probe=can_probe, + ) + + +def set_response(s: WorkoutSet) -> SetResponse: + return SetResponse( + id=s.id, + set_index=s.set_index, + target_percent=s.target_percent, + target_reps=s.target_reps, + target_weight_kg=s.target_weight_kg, + is_failure=s.is_failure, + completed=s.completed, + actual_reps=s.actual_reps, + actual_weight_kg=s.actual_weight_kg, + ) + + +def exercise_response(ex: WorkoutExercise) -> ExerciseResponse: + return ExerciseResponse( + id=ex.id, + order_index=ex.order_index, + name=ex.name, + kind=ex.kind, + prescribed_weight_kg=ex.prescribed_weight_kg, + weight_locked=ex.weight_locked, + needs_weight_input=ex.needs_weight_input, + pending_prompt=ex.pending_prompt, + weight_step_kg=weight_step_for(ex.name, kind=ex.kind), + sets=[set_response(s) for s in sorted(ex.sets, key=lambda x: x.set_index)], + ) + + +def workout_summary(w: Workout) -> WorkoutSummary: + return WorkoutSummary( + id=w.id, + cycle_number=w.cycle_number, + week_number=w.week_number, + day_index=w.day_index, + scheduled_date=w.scheduled_date, + status=w.status, + exercise_count=len(w.exercises) if w.exercises is not None else 0, + ) + + +def maybe_expire_pause(db: Session, workout: Workout, now: Optional[datetime] = None) -> Workout: + now = now or datetime.utcnow() + if workout.status == "paused" and workout.paused_at: + limit = workout.paused_at + timedelta(minutes=settings.pause_limit_minutes) + if now > limit: + workout.status = "abandoned" + workout.paused_at = None + workout.rest_started_at = None + db.commit() + db.refresh(workout) + return workout + + +def workout_detail(db: Session, workout: Workout, today: Optional[date] = None) -> WorkoutDetail: + today = today or date.today() + now = datetime.utcnow() + workout = maybe_expire_pause(db, workout, now) + + active = ( + db.query(Workout) + .filter( + Workout.cycle_number == workout.cycle_number, + Workout.id != workout.id, + Workout.status.in_(["in_progress", "paused"]), + ) + .first() + ) + can_start = workout.status == "planned" and workout.scheduled_date == today and active is None + + can_resume = False + pause_expires_at = None + if workout.status == "paused" and workout.paused_at: + pause_expires_at = workout.paused_at + timedelta(minutes=settings.pause_limit_minutes) + can_resume = now <= pause_expires_at and workout.scheduled_date == today + + rest_elapsed = 0 + if workout.status == "in_progress" and workout.rest_started_at: + rest_elapsed = max(0, int((now - workout.rest_started_at).total_seconds())) + + return WorkoutDetail( + id=workout.id, + cycle_number=workout.cycle_number, + week_number=workout.week_number, + day_index=workout.day_index, + scheduled_date=workout.scheduled_date, + status=workout.status, + started_at=workout.started_at, + paused_at=workout.paused_at, + completed_at=workout.completed_at, + current_exercise_index=workout.current_exercise_index, + current_set_index=workout.current_set_index, + rest_started_at=workout.rest_started_at, + rest_elapsed_seconds=rest_elapsed, + can_start=can_start, + can_resume=can_resume, + pause_expires_at=pause_expires_at, + exercises=[exercise_response(e) for e in sorted(workout.exercises, key=lambda x: x.order_index)], + ) + + +def load_workout(db: Session, workout_id: int) -> Workout: + def _fetch() -> Workout | None: + return ( + db.query(Workout) + .options(joinedload(Workout.exercises).joinedload(WorkoutExercise.sets)) + .filter(Workout.id == workout_id) + .first() + ) + + workout = _fetch() + if not workout: + raise HTTPException(404, "Тренировка не найдена") + if sync_accessory_weights(db, workout): + db.commit() + workout = _fetch() + assert workout is not None + return workout + + +def sync_accessory_weights(db: Session, workout: Workout) -> bool: + """Подтянуть сохранённый вес каждого вспомогательного из AccessoryState.""" + changed = False + for ex in workout.exercises: + if ex.kind != "accessory": + continue + if any(s.completed for s in ex.sets): + continue + state = ( + db.query(AccessoryState) + .filter( + AccessoryState.cycle_number == workout.cycle_number, + AccessoryState.exercise_name == ex.name, + ) + .first() + ) + if not state: + continue + weight = state.pending_weight_kg if state.pending_weight_kg is not None else state.weight_kg + if weight is None: + continue + if ( + ex.prescribed_weight_kg != weight + or ex.needs_weight_input + or ex.weight_locked != bool(state.locked) + ): + ex.prescribed_weight_kg = weight + ex.needs_weight_input = False + ex.weight_locked = bool(state.locked) + for s in ex.sets: + if not s.completed: + s.target_weight_kg = weight + changed = True + return changed + + +def start_workout(db: Session, workout: Workout, today: Optional[date] = None) -> Workout: + today = today or date.today() + if workout.scheduled_date != today: + raise HTTPException(400, "Начать можно только сегодняшнюю тренировку") + if workout.status not in ("planned",): + raise HTTPException(400, "Тренировку нельзя начать") + active = ( + db.query(Workout) + .filter( + Workout.cycle_number == workout.cycle_number, + Workout.status.in_(["in_progress", "paused"]), + ) + .first() + ) + if active: + raise HTTPException(400, "Уже есть активная тренировка") + workout.status = "in_progress" + workout.started_at = datetime.utcnow() + workout.current_exercise_index = 0 + workout.current_set_index = 0 + db.commit() + db.refresh(workout) + return load_workout(db, workout.id) + + +def pause_workout(db: Session, workout: Workout) -> Workout: + if workout.status != "in_progress": + raise HTTPException(400, "Пауза доступна только во время тренировки") + workout.status = "paused" + workout.paused_at = datetime.utcnow() + workout.rest_started_at = None + db.commit() + return load_workout(db, workout.id) + + +def resume_workout(db: Session, workout: Workout, today: Optional[date] = None) -> Workout: + today = today or date.today() + workout = maybe_expire_pause(db, workout) + if workout.status == "abandoned": + raise HTTPException(400, "Тренировка остановлена: пауза дольше 15 минут") + if workout.status != "paused": + raise HTTPException(400, "Тренировка не на паузе") + if workout.scheduled_date != today: + raise HTTPException(400, "Продолжить можно только в тот же день") + assert workout.paused_at + if datetime.utcnow() > workout.paused_at + timedelta(minutes=settings.pause_limit_minutes): + workout.status = "abandoned" + db.commit() + raise HTTPException(400, "Тренировка остановлена: пауза дольше 15 минут") + workout.status = "in_progress" + workout.paused_at = None + db.commit() + return load_workout(db, workout.id) + + +def set_exercise_weight(db: Session, workout: Workout, exercise: WorkoutExercise, weight_kg: float) -> Workout: + if exercise.kind != "accessory": + raise HTTPException(400, "Вес задаётся только для вспомогательных") + if workout.status == "completed": + raise HTTPException(400, "Выполненную тренировку менять нельзя") + if exercise.sets and all(s.completed for s in exercise.sets): + raise HTTPException(400, "Это упражнение уже выполнено") + weight = round_exercise_weight(weight_kg, exercise.name, kind=exercise.kind) + exercise.prescribed_weight_kg = weight + exercise.needs_weight_input = False + for s in exercise.sets: + if not s.completed: + s.target_weight_kg = weight + + apply_accessory_weight_to_future( + db, + cycle_number=workout.cycle_number, + exercise_name=exercise.name, + weight_kg=weight, + from_date=workout.scheduled_date, + lock=False, + ) + db.commit() + return load_workout(db, workout.id) + + +def adjust_completed_set( + db: Session, + workout: Workout, + exercise: WorkoutExercise, + workout_set: WorkoutSet, + *, + actual_weight_kg: float, + actual_reps: int, +) -> Workout: + if exercise.kind != "accessory": + raise HTTPException(400, "Править можно только вспомогательные") + if not workout_set.completed: + raise HTTPException(400, "Можно править только выполненный подход") + workout_set.actual_weight_kg = round_exercise_weight(actual_weight_kg, exercise.name, kind=exercise.kind) + workout_set.actual_reps = actual_reps + db.commit() + return load_workout(db, workout.id) + + +def _advance_pointer(workout: Workout, exercises: list[WorkoutExercise]) -> bool: + """Advance to next incomplete set. Returns True if workout completed.""" + for ei, ex in enumerate(exercises): + for s in sorted(ex.sets, key=lambda x: x.set_index): + if not s.completed: + workout.current_exercise_index = ei + workout.current_set_index = s.set_index + return False + return True + + +def _analyze_accessory(db: Session, workout: Workout, exercise: WorkoutExercise) -> None: + if exercise.kind != "accessory": + return + if exercise.weight_locked: + return + if any(not s.completed for s in exercise.sets): + return + + all_hit = all( + (s.actual_reps or 0) >= (s.target_reps or 0) for s in exercise.sets if s.target_reps is not None + ) + if not all_hit: + exercise.pending_prompt = "choose_weight_missed" + else: + exercise.pending_prompt = "confirm_easy" + + +def complete_set( + db: Session, + workout: Workout, + *, + actual_reps: Optional[int] = None, + actual_weight_kg: Optional[float] = None, +) -> Workout: + if workout.status != "in_progress": + raise HTTPException(400, "Тренировка не активна") + + exercises = sorted(workout.exercises, key=lambda x: x.order_index) + if workout.current_exercise_index >= len(exercises): + raise HTTPException(400, "Нет текущего упражнения") + + exercise = exercises[workout.current_exercise_index] + if exercise.pending_prompt: + raise HTTPException(400, "Сначала ответьте на вопрос по упражнению") + if exercise.kind == "accessory" and exercise.needs_weight_input: + raise HTTPException(400, "Сначала введите рабочий вес") + + sets = sorted(exercise.sets, key=lambda x: x.set_index) + current = next((s for s in sets if s.set_index == workout.current_set_index), None) + if not current or current.completed: + # find first incomplete + current = next((s for s in sets if not s.completed), None) + if not current: + raise HTTPException(400, "Нет активного подхода") + + if exercise.kind == "bench" or exercise.kind == "failure": + current.completed = True + current.completed_at = datetime.utcnow() + current.actual_weight_kg = current.target_weight_kg + current.actual_reps = current.target_reps + else: + if actual_reps is None: + raise HTTPException(400, "Укажите выполненные повторения") + weight = actual_weight_kg if actual_weight_kg is not None else exercise.prescribed_weight_kg + if weight is None: + raise HTTPException(400, "Не задан вес") + current.completed = True + current.completed_at = datetime.utcnow() + current.actual_reps = actual_reps + current.actual_weight_kg = round_exercise_weight(weight, exercise.name, kind=exercise.kind) + + # Check if exercise finished → maybe prompt + if all(s.completed for s in sets): + _analyze_accessory(db, workout, exercise) + if exercise.pending_prompt: + workout.rest_started_at = None + db.commit() + return load_workout(db, workout.id) + + done = _advance_pointer(workout, exercises) + if done: + workout.status = "completed" + workout.completed_at = datetime.utcnow() + workout.rest_started_at = None + _maybe_finish_cycle(db, workout) + else: + workout.rest_started_at = datetime.utcnow() + + db.commit() + return load_workout(db, workout.id) + + +def resolve_prompt( + db: Session, + workout: Workout, + exercise: WorkoutExercise, + *, + choice: str, + weight_kg: Optional[float] = None, + increase_kg: Optional[float] = None, + decrease_kg: Optional[float] = None, +) -> Workout: + if workout.status != "in_progress": + raise HTTPException(400, "Тренировка не активна") + if not exercise.pending_prompt: + raise HTTPException(400, "Нет активного вопроса") + + prompt = exercise.pending_prompt + today = workout.scheduled_date + current_weight = exercise.prescribed_weight_kg + step = weight_step_for(exercise.name, kind=exercise.kind) + + if prompt == "confirm_easy": + if choice == "easy_no": + if current_weight is None: + raise HTTPException(400, "Вес не задан") + apply_accessory_weight_to_future( + db, + cycle_number=workout.cycle_number, + exercise_name=exercise.name, + weight_kg=current_weight, + from_date=today, + lock=True, + ) + exercise.weight_locked = True + exercise.pending_prompt = None + elif choice == "easy_yes": + exercise.pending_prompt = "choose_weight_easy" + db.commit() + return load_workout(db, workout.id) + else: + raise HTTPException(400, "Неверный выбор") + + elif prompt == "choose_weight_easy": + if current_weight is None: + raise HTTPException(400, "Текущий вес не задан") + if increase_kg is None or increase_kg <= 0: + raise HTTPException(400, "Укажите, на сколько поднять вес") + bump = round_to_plate(increase_kg, step) + if bump < step: + bump = step + weight = round_to_plate(current_weight + bump, step) + if weight <= current_weight: + weight = round_to_plate(current_weight + step, step) + apply_accessory_weight_to_future( + db, + cycle_number=workout.cycle_number, + exercise_name=exercise.name, + weight_kg=weight, + from_date=today + timedelta(days=1), + lock=False, + ) + exercise.pending_prompt = None + + elif prompt == "choose_weight_missed": + if current_weight is None: + raise HTTPException(400, "Текущий вес не задан") + if decrease_kg is None or decrease_kg <= 0: + raise HTTPException(400, "Укажите, на сколько снизить вес") + drop = round_to_plate(decrease_kg, step) + if drop < step: + drop = step + weight = round_to_plate(current_weight - drop, step) + if weight >= current_weight: + weight = round_to_plate(current_weight - step, step) + if weight < step: + weight = step + apply_accessory_weight_to_future( + db, + cycle_number=workout.cycle_number, + exercise_name=exercise.name, + weight_kg=weight, + from_date=today + timedelta(days=1), + lock=False, + ) + exercise.pending_prompt = None + else: + raise HTTPException(400, "Неизвестный вопрос") + + exercises = sorted(workout.exercises, key=lambda x: x.order_index) + done = _advance_pointer(workout, exercises) + if done: + workout.status = "completed" + workout.completed_at = datetime.utcnow() + workout.rest_started_at = None + _maybe_finish_cycle(db, workout) + else: + workout.rest_started_at = datetime.utcnow() + + db.commit() + return load_workout(db, workout.id) + + +def _maybe_finish_cycle(db: Session, workout: Workout) -> None: + remaining = ( + db.query(Workout) + .filter( + Workout.cycle_number == workout.cycle_number, + Workout.status.in_(["planned", "in_progress", "paused"]), + Workout.id != workout.id, + ) + .count() + ) + # also count if this one not yet marked - it's being completed + unfinished_others = ( + db.query(Workout) + .filter( + Workout.cycle_number == workout.cycle_number, + Workout.id != workout.id, + Workout.status.notin_(["completed", "abandoned"]), + ) + .count() + ) + if unfinished_others == 0: + profile = db.query(Profile).first() + if profile: + profile.phase = "rest" + # wait for user to choose rest days via API; temporarily set probe far + profile.probe_available_at = None + profile.rest_until = None + + +def start_rest_phase(db: Session, profile: Profile, rest_days: int) -> Profile: + if profile.phase not in ("rest", "probe"): + # allow if cycle just finished + unfinished = ( + db.query(Workout) + .filter( + Workout.cycle_number == profile.cycle_number, + Workout.status.notin_(["completed", "abandoned"]), + ) + .count() + ) + if unfinished > 0 and profile.phase == "active": + raise HTTPException(400, "Цикл ещё не завершён") + if rest_days < settings.rest_min_days or rest_days > settings.rest_max_days: + raise HTTPException(400, f"Отдых от {settings.rest_min_days} до {settings.rest_max_days} дней") + now = datetime.utcnow() + profile.phase = "rest" + profile.probe_available_at = now + timedelta(days=rest_days) + profile.rest_until = profile.probe_available_at + db.commit() + db.refresh(profile) + return profile + + +def submit_probe( + db: Session, + profile: Profile, + *, + one_rm_kg: float, + start_date: date, + training_weekdays: Optional[list[int]] = None, +) -> Profile: + now = datetime.utcnow() + if profile.phase == "rest" and profile.probe_available_at and now < profile.probe_available_at: + raise HTTPException(400, "Ещё рано для проходки") + profile.phase = "probe" if profile.phase == "rest" else profile.phase + + weekdays = training_weekdays or profile.training_weekdays + if not weekdays: + raise HTTPException(400, "Не заданы дни тренировок") + + one_rm = round_to_plate(one_rm_kg) + new_cycle = profile.cycle_number + 1 + profile.one_rm_kg = one_rm + profile.training_weekdays = weekdays + profile.start_date = start_date + profile.cycle_number = new_cycle + profile.phase = "active" + profile.rest_until = None + profile.probe_available_at = None + profile.onboarded = True + + generate_schedule( + db, + one_rm_kg=one_rm, + training_weekdays=weekdays, + start_date=start_date, + cycle_number=new_cycle, + ) + db.commit() + db.refresh(profile) + return profile diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..5de0e99 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +sqlalchemy==2.0.36 +psycopg[binary]==3.2.3 +pydantic==2.10.4 +pydantic-settings==2.7.0 +passlib[bcrypt]==1.7.4 +bcrypt==4.0.1 +python-jose[cryptography]==3.3.0 +python-multipart==0.0.20 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..daceb9f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: benchpress + POSTGRES_PASSWORD: benchpress + POSTGRES_DB: benchpress + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U benchpress -d benchpress"] + interval: 5s + timeout: 5s + retries: 10 + + backend: + build: ./backend + environment: + DATABASE_URL: postgresql+psycopg://benchpress:benchpress@db:5432/benchpress + SECRET_KEY: change-me-in-production-benchpress-secret + CORS_ORIGINS: http://localhost:5173,http://localhost:3000,http://localhost,https://localhost,capacitor://localhost + ports: + - "8000:8000" + depends_on: + db: + condition: service_healthy + + frontend: + build: ./frontend + ports: + - "3000:80" + depends_on: + - backend + +volumes: + pgdata: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..62b26e1 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.git +*.md diff --git a/frontend/.env.android b/frontend/.env.android new file mode 100644 index 0000000..8bbb542 --- /dev/null +++ b/frontend/.env.android @@ -0,0 +1,2 @@ +# Offline V2: данные на устройстве (IndexedDB), сервер не нужен. +VITE_OFFLINE=true 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/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/ANDROID.md b/frontend/ANDROID.md new file mode 100644 index 0000000..ccdfe47 --- /dev/null +++ b/frontend/ANDROID.md @@ -0,0 +1,32 @@ +# Сборка Android APK (Capacitor) — Offline V2 + +APK работает **без сервера**: PIN, программа и прогресс хранятся на устройстве (IndexedDB). + +## JDK для Gradle +Нужен **JDK 21** (не Java 25 из Android Studio). +Уже скачан в: `%USERPROFILE%\.jdks\temurin-21` + +```powershell +$env:JAVA_HOME = "$env:USERPROFILE\.jdks\temurin-21" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +``` + +## Сборка APK + +```powershell +cd C:\Dev\Benchpress\frontend +npm run cap:sync + +cd android +$env:JAVA_HOME = "$env:USERPROFILE\.jdks\temurin-21" +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +.\gradlew.bat assembleDebug +``` + +APK: +`C:\Dev\Benchpress\frontend\android\app\build\outputs\apk\debug\app-debug.apk` + +`.env.android` содержит `VITE_OFFLINE=true` — сервер и `VITE_API_BASE_URL` не нужны. + +## Online-режим (опционально) +Уберите `VITE_OFFLINE` / поставьте `false` и задайте `VITE_API_BASE_URL`, затем снова `cap:sync` + сборка. Для этого нужен запущенный backend (`docker compose up -d`). diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b44f7ff --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/frontend/android/.gitignore b/frontend/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/frontend/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/frontend/android/app/.gitignore b/frontend/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/frontend/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle new file mode 100644 index 0000000..3683d10 --- /dev/null +++ b/frontend/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "com.benchpress.secret" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.benchpress.secret" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/frontend/android/app/capacitor.build.gradle b/frontend/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/frontend/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/frontend/android/app/proguard-rules.pro b/frontend/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/frontend/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/frontend/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/frontend/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/frontend/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5962264 --- /dev/null +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/android/app/src/main/java/com/benchpress/secret/MainActivity.java b/frontend/android/app/src/main/java/com/benchpress/secret/MainActivity.java new file mode 100644 index 0000000..38527a8 --- /dev/null +++ b/frontend/android/app/src/main/java/com/benchpress/secret/MainActivity.java @@ -0,0 +1,15 @@ +package com.benchpress.secret; + +import android.os.Bundle; + +import androidx.core.view.WindowCompat; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowCompat.setDecorFitsSystemWindows(getWindow(), true); + } +} diff --git a/frontend/android/app/src/main/res/drawable-land-hdpi/splash.png b/frontend/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..e31573b4fc93e60d171f4046c0220e1463075d9e GIT binary patch literal 7705 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 wKmFJ0_JHhk1&0B>;%YXATM literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-land-mdpi/splash.png b/frontend/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a64923ea1a0565d25fa139c176d6bf42184e48 GIT binary patch literal 4040 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 XyOn+%7f`N3b2T^T3uj5+eShz7v)7qy literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-land-xhdpi/splash.png b/frontend/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..807725501bdd92e94e51e7b2b0006f69e0083a0b GIT binary patch literal 9251 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/frontend/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..14c6c8fe39fcd51a0414866ad28cbe8ff3acb060 GIT binary patch literal 13984 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®q@N z14_H8ofW)l{|J8q+a)eH)I0r)>WXdzV%7J>PA~6_J)KLT90iYa^K=Wz7D!OybzqSru=f4?|KFl;Y)gP_H6V4x`~kZ6fE(xM1&;?72-TZNk+0 zr+Crr5yl%Iy@vfmt3eYFl!jIvPGFz^8Ek+2`48O1_pCX3xNWh-zBa{rIcc%+=|XVj zANYTg&s}TKb#OztQrCW(Xk?V^i{`q~%HtcveTxq(_HKeC9GzrtguMT4Nvs@KakPTA z9>*8bBZmLz`lK5=l)=b|=dT3a5ag^a1^znZyx5QKfUb1b9yacArRp%3@QWo(hrsCU z-K!-=jDmv!zb7XT>)r|-Z0Ry}lk2;dk-ECqMwr_nKN#x*X6~B5hVIN>6$1HwBz3Of z=Pk){AL5*=d90f17_qZEJLm;Q%WMdX=*N&!ki@E&cy7?>{1ssAH(tACtp*r@d^til z)x(1#6(kPD+joSF&J3sxJU@{-sWCS+pZq{Gsx=?z4wP;>?)1yHv0?X?VP{}cX4~aH zxeBPKw_rgW8rvewS1W2#^y+c>-183iMbJCqc38RN_o~__9-n|jcd&oA`m7*&Fqqpc z;Tev*0LS-ZK47Sq1unfvP1S43uA12P?PJmI8BeTYPr~R*tYUm^0;U%Hmu?bSZHEK6 zPjsW=E67Kq-&trmf;)UkmRABH2U)V)-eRT$j(%G12lLMsThSsU10iP#{)ZnvjzN$d z*K%P3`}oqyvpWP~venr>3viH8^`)Ma*=B31hw*Q+tqE>i2y7w!(o^lI^Yss^=tHW( z;cnCT(%B1gLz+TRGW9roFjI1EQTu-u`(f#RmZ8;FSN(bsC1J;+(i_R6mrW=yYx$cy z#%QKVrEx~kVMg~yo?^N28Wnk6x%L;J8i|*|ANEiNjq(Vhzuzl3ikpA*G!Z}kLAzAI z9qnySo%D|AuJj12%h;Otqjs(>LPj?rNdeU8so>P(C>XMzlho94ZD#w=cCOOU;=3&^ zsqAG!i{~lY271D|m>ztPV`)X@FO_;`wPjppYNQpM+ncvtz1lZjN>!Q^*I}T%uP78Z7tbV2$q3W_)14=kLFyJ z1GqL6T>ClgeZorL!}xP4f%OB_EsmJ`uw7dGWNV9OLlhb|UMpVhc{4@Bhh`tO!ZqzD zhusd<=K^ah!L@gQ?6dOpI-ge^e>S5W9eII57Zu16eU?GRbgKTeVk9yS{iK|O(zLR> zheb?;jwGCHS80NCn=jKxgJ>}qu4l%5NPihjzazGv#J?Jcyl;<#IW&x4mm>nrW8>}C z3U@aeD~)*F(0o^2{GnKVm$Jr#aZE ztl~TOkM^SdzJapQ((!-i8b!RkVQBKkL`2ZCBuy!qI1L{3Er526plVols~68U-^9Px zR(3{j;Z9RHX^muc0dUywJ|`yyZFf=k&-Gb#m4u73Lm5Ks%BfHj%2|gjn#i> zLC5pO$2Em9H;qoKQmMtl<@wgtPF1%2HariD5O~u>8=^*J&au~JH%Ih@&2Uging3U_ z0bzfKucW$ZHSx}!#buB?+-J)%RQbbXM-!BJTS&#dU_@lxU6>te2O+9 z@F{F{Nb!;{Cd`Gx+$G?11aB~S#wIH%D=*=7f7H@D@%B1)&bF$@t3JDq4l*%(wJTlh zo`?uMq{YilKUewPNaC)GuOr<8j9&ofqRU__BRUX^x8Cj3a;a$rXzgXqW>LR#CUn%~m)t zYC&ol(gAkbc^fd`xWU&bk5vT6KbFmsR=O78Bn%t7 znbw&=c+|T&#r+bls5rU6D#HMvqA<|;)BV%jOMonkm^p$7Vcel-Wwn$=uAJv&(8W>% z9))Fxpl*(%E#wFm_m!U~2HqgZs^2vaGeY(UfYKrSHV}w^D0N6!se5Ewy)Yy-!(2

aKj2hWG7>znxs|SE zN4rHtiSPqLskWp(?(_YYwgq+1@8v+~8As|(bC>$D(atG3ZE8-ZM3SVcg|vHQz$I=!(A`k`5= zOqR>&%G)$)k*QLz7MTB9wleWpv&N9Sta64wy}3Ytd?x!Ja8z>(z~(3UNFu^eFmn#6 zw!!gUxOuZi$PQIs*ixfZR3iLyADJ z5&s%tPfk>V!x|A-;oq%1!yk9H$UBP0ToA*EDtz(^!_AnF1bBQ7joj|? z5b)gSI8c8O$PYFE!vXJ<4gebg*9G9P2wcB{#kv0FItc5T@PDNo)}Rh4Us}L{e}xzW zhwt`)j`M)mP=G6H0;^&q=I0{jU%bIRkF#uLF;{vVC&H|_uc literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/frontend/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..244ca2506dbe0fd8f6a05520ac7d1a629ea81438 GIT binary patch literal 17683 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/frontend/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..bfabe6871a17a5e95b78fb30d49b7d2b4d2fe4c0 GIT binary patch literal 13346 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

VwO4Hu8X}%XHUAz_J@50rzbkIsat>4oWtQt< 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/frontend/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..6929071268eb03ee0f088142b6523566b78550e2 GIT binary patch literal 17489 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 KR&><$`~L^lv%S~= literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/frontend/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/frontend/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/frontend/android/app/src/main/res/drawable/ic_launcher_background.xml b/frontend/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/frontend/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/android/app/src/main/res/drawable/splash.png b/frontend/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a64923ea1a0565d25fa139c176d6bf42184e48 GIT binary patch literal 4040 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 XyOn+%7f`N3b2T^T3uj5+eShz7v)7qy literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/layout/activity_main.xml b/frontend/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/frontend/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2777ed4552a31fcdb2d7ab9590e3ece2eac5ed77 GIT binary patch literal 3363 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 BSCs$& literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..804df2894c265c966d2100b6293e95cb44fb8666 GIT binary patch literal 10874 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 WA>UEVY`~pTIOW$G@-?#NVgCa{dKOs# literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..2777ed4552a31fcdb2d7ab9590e3ece2eac5ed77 GIT binary patch literal 3363 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 BSCs$& literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d2157f8406fedca50ddd5028fa29a13fbfc423c0 GIT binary patch literal 1988 zcmaJ?`#TeQ93D={-NmAfRea#`Yp>`asECXw8! zq@g02T*{?5ts+{oL(67Z>FoRi=XpNg&*zu#^L)SW`#$gc$@amzYiocs004ltrw7_s zF&+Lp>Z*! zpSQPeLPEl;fkNxZXIa|Jf&%3H{5*Ld`yRE#7cjaI?~lVF>+tm)IvqfXin`O*wx2?y zSsEJ~FOrgMv+8FN+rI`Tkjdm1^ihXv92@418z`=OUiHu|CqF#iNE_^W6O6cYOdt>> zZtEe#BoaweUuXeoKd7U<{jWC-L4)@8_M(}YCvw?x(RxFJ{brDsnRC80nJ4Y)>MDbs z>Z0-b6M}=miU;F7J+lRlC;uUWRn{a62KB_&tb^y?dFX;&D>gQgAEn9<4wWOyRkcBu zc-!5(cR4vZHT#E$hr^<3ggqDS?Ce^=?^jk<3OoKoLZxyItg$}Fj%iJdkNf=E{8{_x zQ8642kC<+YWR{nEw}y{Yb2vLSbaj6{Q9{@NEV@(HQKz~u-5$H+J$B0}3SCh72ZOJ< zAzgGI7#J`F0(U-@%jILarjF?5>FGdyJrxYnxp(;^O zIto!EfhUR0pNAK#3Pi;L``D;b)a+8Rk#S6Pmb_~c}G*qJleWJY@kA%_ugc;Il- z&yDsZ<t2y7eFHE0Z#Fm&DvtT`9K9bu~3jySW0(rKKg785bB>d{5;@VWCyWr`g$u9UVl&1(Aq3r^X)~oO<)7NVX){ExSKZURE~T z+}tdhpYIb6mlk#iI?u-;kxD|LaHZ_<#fJ?I*BgU6&)C`PY-raT5 zmtkOfpRb=^{i24Z=Fz>S*nZV?(|Dp{;=w z``y8c75c6#KKbLzt6h7wCjVZFsiEJsyLb5f(#h+&xpVpXdNJ~;*6?|U43&%$2htTA zwPi?&Ub2^$7k&#)`R9)xYcJW3`xvRtA7}?<-(O^E8arEC_b2Om!=J+90Q8E0fB+zt z&1RdKnAjX5X{wtU8d_FLL{pAzDvdVB=VRVKh$_$c*b+9FwD9`2&X#>9T0F|;$ezYo z29%s``t$XbVxWiH~J`HvFcvsB)4sr zjR}Un69_W%6aSgE;)g6>R#E%%b)qgbx1s`43{SXQvLgQEZd-J`;iNv;1A_r>xsslK zyJQ;3IqWQ4ki;^{ZC*2Be{twn0n@JI1@>gjkDCk%$&)P+VO6MFH`3Y{d}$0EyTJ zh;a9ZT@HA%8lfpk=82BlprTFs$Hx3jy4u?(Y}p%C03C*M(>H|@VLE%Sf1@AoslH0J z$-*X%*WiF3B9unYRT!go1CBN)4zle02?QI9bi9&Ar2*YH>GO+?R6M{U{jFOLpe_K9 zzxFXne|oFLu^f@cdU*U+qax@qn%4`8iriyjVkD9Uzx81U(4Vt*BF2N?y^1Z*ssY~; zo&V~MMga9$>3}3QZE^u^OVG*QN*WzxWM*a?U(3n4t&dK5_B8JE%Yt5XTD)v2Xye;V zdOXMJ+`Ifu^$Xmmri*o&J5XW%<8cm9%8?DGB79@}tR0kL2mUdRz=XTtQ=-d;3G{2=OsMy*up!u8gm2Rz|3bau3K>R{sI`s*@dU|L{S9}r&Cq z=A)t9_X@*n*3P&9NMQ~pRgm$C3D2-+;1cQikN!F}^a{P#rdepw@qmD>wHDSDhO4#D z0ozaPFr&21)$LxFKekK$nv}S&^E_`%nUadl05q^NC|~4b0xa^1O`H7A!M6x zt;?P$5_T1^pzj1poj5 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..03bf7836c9de42e1e92a03fa032146c52b54bd58 GIT binary patch literal 6180 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 A?*IS* literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d2157f8406fedca50ddd5028fa29a13fbfc423c0 GIT binary patch literal 1988 zcmaJ?`#TeQ93D={-NmAfRea#`Yp>`asECXw8! zq@g02T*{?5ts+{oL(67Z>FoRi=XpNg&*zu#^L)SW`#$gc$@amzYiocs004ltrw7_s zF&+Lp>Z*! zpSQPeLPEl;fkNxZXIa|Jf&%3H{5*Ld`yRE#7cjaI?~lVF>+tm)IvqfXin`O*wx2?y zSsEJ~FOrgMv+8FN+rI`Tkjdm1^ihXv92@418z`=OUiHu|CqF#iNE_^W6O6cYOdt>> zZtEe#BoaweUuXeoKd7U<{jWC-L4)@8_M(}YCvw?x(RxFJ{brDsnRC80nJ4Y)>MDbs z>Z0-b6M}=miU;F7J+lRlC;uUWRn{a62KB_&tb^y?dFX;&D>gQgAEn9<4wWOyRkcBu zc-!5(cR4vZHT#E$hr^<3ggqDS?Ce^=?^jk<3OoKoLZxyItg$}Fj%iJdkNf=E{8{_x zQ8642kC<+YWR{nEw}y{Yb2vLSbaj6{Q9{@NEV@(HQKz~u-5$H+J$B0}3SCh72ZOJ< zAzgGI7#J`F0(U-@%jILarjF?5>FGdyJrxYnxp(;^O zIto!EfhUR0pNAK#3Pi;L``D;b)a+8Rk#S6Pmb_~c}G*qJleWJY@kA%_ugc;Il- z&yDsZ<t2y7eFHE0Z#Fm&DvtT`9K9bu~3jySW0(rKKg785bB>d{5;@VWCyWr`g$u9UVl&1(Aq3r^X)~oO<)7NVX){ExSKZURE~T z+}tdhpYIb6mlk#iI?u-;kxD|LaHZ_<#fJ?I*BgU6&)C`PY-raT5 zmtkOfpRb=^{i24Z=Fz>S*nZV?(|Dp{;=w z``y8c75c6#KKbLzt6h7wCjVZFsiEJsyLb5f(#h+&xpVpXdNJ~;*6?|U43&%$2htTA zwPi?&Ub2^$7k&#)`R9)xYcJW3`xvRtA7}?<-(O^E8arEC_b2Om!=J+90Q8E0fB+zt z&1RdKnAjX5X{wtU8d_FLL{pAzDvdVB=VRVKh$_$c*b+9FwD9`2&X#>9T0F|;$ezYo z29%s``t$XbVxWiH~J`HvFcvsB)4sr zjR}Un69_W%6aSgE;)g6>R#E%%b)qgbx1s`43{SXQvLgQEZd-J`;iNv;1A_r>xsslK zyJQ;3IqWQ4ki;^{ZC*2Be{twn0n@JI1@>gjkDCk%$&)P+VO6MFH`3Y{d}$0EyTJ zh;a9ZT@HA%8lfpk=82BlprTFs$Hx3jy4u?(Y}p%C03C*M(>H|@VLE%Sf1@AoslH0J z$-*X%*WiF3B9unYRT!go1CBN)4zle02?QI9bi9&Ar2*YH>GO+?R6M{U{jFOLpe_K9 zzxFXne|oFLu^f@cdU*U+qax@qn%4`8iriyjVkD9Uzx81U(4Vt*BF2N?y^1Z*ssY~; zo&V~MMga9$>3}3QZE^u^OVG*QN*WzxWM*a?U(3n4t&dK5_B8JE%Yt5XTD)v2Xye;V zdOXMJ+`Ifu^$Xmmri*o&J5XW%<8cm9%8?DGB79@}tR0kL2mUdRz=XTtQ=-d;3G{2=OsMy*up!u8gm2Rz|3bau3K>R{sI`s*@dU|L{S9}r&Cq z=A)t9_X@*n*3P&9NMQ~pRgm$C3D2-+;1cQikN!F}^a{P#rdepw@qmD>wHDSDhO4#D z0ozaPFr&21)$LxFKekK$nv}S&^E_`%nUadl05q^NC|~4b0xa^1O`H7A!M6x zt;?P$5_T1^pzj1poj5 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e2bd1c9c6e5ff24311338eb9b89c89c692c16129 GIT binary patch literal 4891 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..03a82f4d737b58d6a9e8c4b330e219a1234afdf1 GIT binary patch literal 16449 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 LRh6ldG7bJes%~=Q literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..e2bd1c9c6e5ff24311338eb9b89c89c692c16129 GIT binary patch literal 4891 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ecaddfec937aff69b99281b79835fc9d4d640267 GIT binary patch literal 8357 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..8f9371036fbf7952c3fa0f493ca4b67c87f919da GIT binary patch literal 29410 zcmeEOWm}bN)0QnLDcvQY(%sUnfPe@{cQ=B7q#)fPAt5Cx-QC@yNJ^Jo}~;%mP1r zdV=#ZM3TDAE>2AB{4Mpt!9ldEt84Go)zvRb_ZH!YFC;u}m%EGA%cz}Bx2K$y^HrE* z`&LN8a~537t@}d0sxX+SGQ@RvbkIyqOw_P+un{w<+#kbfQAb>aH!$8G$C&%4#e04W zo=$*+gG0>Km4___vUt7^wmW@c`W-Yq_+hqM;z!`<&~AjHP%xc4+nH= z?Wrtw^jMUy_#Kw~zUSteCFSSS7vq|H*JaWxQb*IV@jrjgP@>yZ?>X1_L5F}&n%6veY2s=H|vqBfhIVdsJBDQ~~Ff%EQys#1#V_eS5mr z!AOlI!OCCbl{5w-)XA0$`r90Y`jZn@sW3a!_W>Kq_z@$;0W}-p9Nb)QQ z6TyZ3gS&g$*w3Fv>%*z;__q~IjFgZXBuC^(7(ExN8rQZ3RAp^7XAsTxi?hui2XG?de+BMty2BA`|&2> zRNCx<-J`BYYeSleA_4;Lc@-6$RyH<6Uq!sVh=sEPSq?Iymgr}`zby%kV^B4$v72vB ztQmPRoWftx+ua@T^Ss|saV2mliAo|sbf&NiGr8~g5#>X1G@Ae7O)wHz^}ard?c?J3 zBF}9-SsKVRFnw6jXYjWB3CJ14#!1=~jhUTwILzYd@ab2W?JIPqTD)cU#9kdu4l+#!fEaEV#Y0a@?}9Va`HO zhMvJkv}yiT*i(~=i>s1aZFPNJhH$GTQom(7@z2V1U? zkWfNUacH^ke7-`Ou#m&n@1kdxRhpc(vwSCOL&*(n=(rEv-{0O`t0Jdj-(T!ylQ%Rq z&3UV>J?sDR)81CYV+1qh1vFHaVTNqC>CQlDK85}1b1h3 zv&Y3dbBlmVajy0Zi9j#u&%Tn>eSz_=#~VMW4A;|K=WuBi`b68^5?Zr2*x;p8=82a<)kDgkg`s)g)gPUxf zi3CK9fqqdX>tOB-(2+cefOUciE(x| zQczG3eKkA#`*S^0HrtRzulb63puC=ig(anK-Scd@ePscM?Z63C3p1FvGef6$Dbk~8w5=dFl%nxju(tOB5z!t?NvZc zS_NA@6&7yRzrVAyQ@2$C7gt1GLt_=`;X~fxzP|je<>jn{lar_Xtr8D2(OX5s-+qy= zwrTx{ko@mt>x?u&0~;IL8tOiKczAe}B1+Lsj3q%%g#ll=lrKYlI+^zQa}tEP-%w4B zoG^CN^WWs^qN1WM6>j?Y_!#e9B#i#}K|f@@hc8KqgVTP!yPoD*-3LcRZavVuU7pM>S8=fiBiFTBWl4vRWo2b)X=!S< zJ0takSweT4DbtqSx;(QEf)?6~o0cH1z&0#6miIdAtcvR07BL*M36t4MZ*;*KhClY2 zOrk=rLaP3mF)5dMdae7J?N(+AW1{zXf$9u)1K)>3W#jAihV+2m+js8mie>J zVBCb^uR;QHbmQNY^KaO=xIP!c-7(mnEc=`NvVbta^y1>8j%nbueH4W^3my^^2S?0z zDgq*+bKzvU&*b^d&0b{;)Ozz65m-j?CPNoTJ)gxswmng1eD*Bh==gWu>*XZ4$}2Qz z{okk#7llW0vI^doc;P0;fByXKJq(ZszB~+rq$DJ$2E~9t4uzqayi`$9u}L&WMH^-% z#tAY(T*RWHq9Q_hJKgxt6k_b*f3JF$cTvYKE1dvFW&n8^?LivOmYLV@25|f z07$k-iHXagi9IqPdNwgP_cfH9X^o7){i~p$ARC7P36HfahDjStHnx>p)E1v`rNohm zi3F|Fg|<(6swKKQhZ{d={87G=7=3p78uy`NEl60L(&wRkQbh(<11z5n1P0PRabY!ik{y^ zTAuHrtGr6)k$U8wuvE#lo!hITymDXu{HE>G!`RJe)NofviW z26aKE$HgH-wz3AUNRe`OKR-En>Mp9kxK7GP-+ubozC>s=LMq?KgYEkl9*7n-SdJHP zH-9+F`3mEzS?Roj>v(TI&GA1Wud7LJ7M5YCN*pS}Rm~dPKhg|w%~5^EuOB^n^y0fg z##F1t6BYHFMDg;{`>47~=-26O2bhoB5MOJ%_BT|MAGq70&^Om}$v8PxdO$f27HQQS zhiKjVEUk@>NhxeB9ZAu2+_2Xypjo8JZ2<%SSDvD|xw&ii+}zF64z@l%J@MZ``lqe= z6r85$$!TdLpIP;U%U;GlQ`{$)rdT=&@_I;`h0wvqe^s^^u)XxEJuLu(^xxY86$|FM z@8gz?=q-5`R3ohC~eB?xoKWWHYO%rs_Y*E9i@WNgJ9CI zoSmJeQ4L1!H8wVW25^GP`x0eFQ)%w+>OexVeQ%*&%grwI0sP;;fA^nkO|+!&+EJik zoWhqz6ut_6C=IgqSJX9JzP{-8C@4NZPV9Rk1EK zH<*dR{3;P~=+Whe&0qN#Fo)d^FZWu~{5l?L=BgjzS$I`-N01K)(LKPV7)0%&Lbdoh zdkN1twMX%}%kFlhl{7Nm?HCnC8D8t|r>k*5SMq>%Qh*F-%T4`3q{z(+pb#CJQ0bqb*SjIS@-xvee=!{sDXF4@S0K)r%wuEd zezv=Vhl^`Oj*b0;gO|7F=1+qQH_S48MgSKyPoGM~#Kv}<6z2o`iqH%B{(Z~w&6_vG z+ZKGL_zWsh>BZ^s@dOl>^k#r`Kbm}3VNlc4(=#+Mp!xyWi$M`8?n_);+;bL|Vu})! zrP&s%H{5-Feg534Mf>~q%UcsA-fw_*`7jOe=o38+#j{1AJDpfr1u2IgLO2D9e?3v z!PrSMugTsYz+jMwgL5ca&ig^VKX4OUx0p4K}i-ADba-;9v-T54U2aaV!i@Ay#kF| zo8JS0EUmFo(6+s;zqeOuZ>pk4Jwxe*y+>75RUL}1^3|g`k4BmJk%0tODk8?0Phw=T zVhr_tKel$*IlX@`fZXx!DsmO&c6pTM0d~terR>wGCyR@oRuoKV&fT9snxm+X|h!Lg-OI;_xbc#P+=r((3oFv;dPbJ31`Jb!Vra9a&s^sZcIQ<<{gR94l z`sk6GSmH8*`_6O~OQ`|PmZTuwdP&h0Je-`xwC!!{XHTDItG|LGp;Qp4d~E*+1?4Zw zmqE6{iuR+Ey%CdhPxdHLrj9aHG&IfyP9-+>){GddUNaLR1%+*Fbo3%93`R{sLC7g+!-W}Y-?O10 zC+jeIB$=*0cHB0Wj=7y|;tNjjJb(Ti_p$y#s_e<_NExU+YK<_jTpN4dBW55|g+Kf_ zTK1YDj@k>W1W1`p@CshAQ2;k_zlwDPhKGiB5}v(Edd##Dx)6#NRIAQ@>~|l*q}tlXCL12sVl${j zxRq?fA6Yr}&02$fd7-LfnRMzaKcqf?A9FE^zPs6DM@>V+-Z?f?|K1A6_9Li0p#WTS z7IHiZqkL;$07afF0;uYPmE-ID?()>;8U^>+EE)#JxTxbxIk{M46O)%kg@uFmjt07o zOV7kA{23KTYl~7rni`2X~T! z&Ci@6R(L=Ebc@uI54Q*cOtfoiGP$(Ma!!QvtpL9KsVb)(m{X1T@BXHRcGnls)|%(w%&M9@=q zog<{d`an~O*RIgZwXij~=vlUjMXADE2bkvD;Gq2E_^+v{Ddczx;AgHcw6q%T2gG?` z--|0QF7}tCw(z?Ccf$PO!GmEy<`Vlm*D>|`2bxHzGOZd)=}imWbq>o99GAN>SYpNG zm2s4FLj4+Asq_sE4S&I573bvU2Hud8{`~p#h?A4E;dZ|RlN_3%Eie>{D$8+8|Jc}A zR@=OIX00`~Vr`8E00${L7`%njS$&})A->pOv+T_&0t!JQoW)QZjEZT<_YW9vtnTG=eWPl zB$z0z3y@>Xh#sugr6=u4=Jf#+<7LvU%+qjot}cMds{zDz2Eg#SfZ;u?pO*8_rH~ZVIoGgzQQx7l3&oq{K14*+ z2F;_^ts?;AJ;*sV&3Y|fKRG!#0!WC7|12<0*|&Tm{_k{1*}5faUU$I*dzcutc}Wj1 zc4%;rOvLSYL+0EI8pVmWj!s7ExB$%JLBP(VH`kZf7xUuxE8Q**O~TUdYs$4dl;LW_bzDCqh8rAbq34M1td;1xY&IQ zFg?U%eu6D}-flGfEAKVUVY@$C468q@^y6l)>-x1HSu#y9AS_{y6gkSvhvD(W!@RZ; zBtLi(Fxt*`|AFU^eJN2Q0!KnKP0geNAYP-$b)+)2#QFmbo|o0#pws_Mgvra*t^Bsh z=Xc;@sjJuh`}fCJ`eN;pzI+jS1rTy$x{5L2!fbG8h_jPI%!?x;B7%YoP*~TfI*6~n zP-ADN#>VNFNp2u7iCtkl)u=R&E=7Iup1U!D{JE1Tg`Gt63;#rAlL5Zx&)ct**wh+a z_H>)%5?C~(rKD6X#=iAEhSzM0ii)Cveuufdv^0IZIli?8qd56=_m9BR9r9Pp5cmJM z*b(TMwD9d^tWvcs4RTw1T-LmQ?^J8}JuI_6w!!c_Ks-H>fi?wfJp2{hyeSO0TdbRSbt)cre?W?5EYVe|0V4zHKpm%&AN4nF`g&f!9 zKY?QSnB|d$(QNbe`EsNYH_6bUT9M`}AGSu%OCH0uU;c8h;*8_>4-UizzXeA|j@Vae z3SI3lcI3e5<_3-Q8EV2>Zg#dmeO$i3LnldVFj5UI|)J4?MMkUODt z6j}j$1B3Dtl#j4f#@87>`VOPrtZzKMvZeeX*=nbS`5vcL)TESbo;`7)zGYp`)hK?G0mI@{GLf}eXvM-K8|JTcI-^SPft9ftBt%4O3{+Gc zvxT6l862PZkev@#=LfCF$4(0ArY0t0A(=_xl|g*ss;;hl3B=6`YH4qmsbIv zf3vhCBR>mN3fsDQvaL;w$8l}YwsfXV_pMs^Jxz?IaivVJzhl`AV3ygIXjb+AnV=M1h z&%iaz=HT@7MjyXp@uSZgx80oJUT^PsrcfF@_kvjCAxwNlQb+;XgJY_|f)6 z^DZsvVulyc$$4E*P=yJDJBNoKIqMX~w(E&51~x;3^hDA@namo5c>p0BK06kP4Mhz&7ao{CpkN zKlAxbPZSn!cf<4jV4=8Llr&+_e+de99tVLgzMczjO1YRdEBT!N9sk7^az7QUhlcoa zV@&Or9pK3a>tQj30rkL#OAX>spf{*|RI>q~kp;uBVAAh@PEaT^x1LP*a~GK77b%!tsT`@cNyj zw|r~+r(RX$$rDLF5kqFVB4`{sAcgaZ9W6JlyaWlGmN?)583hIV@Uz_M0v>|5^=B}J zOo2qtktzrYZbR!kTXJ8qL7UFS>PSjX-txIfNlkqUmGB$)u+FnzKkK^WunSpIxYE$k zVIFOfOgz&YCI{-S@A2-oTNmc%|0PH6NaD8IyaG(m5S`PB zbkg0`g(T>8MI=Qp`o%`dgQCa{Br6^uA*5r#tg3-M?!?8$F7p9C;D__lb^yWRumzqd zy|?#mJit;>bOAF>(c0jR48Mm6m&bqK{DE@_YXSx`3Lt#YdcOJk88YsiaQ+3BIChd* z;3M+WJBilw=IY`aN~0e|Dg$#EToSB%B|2jVfor?V%Wu*sHPLA67iOtEPz@yO>+5Cq zzeIVX3(Qs9%p777z-ZZ&l9X(Xa#v*&_-x{RuA)FD^BoZCW3SH@di4zrX5h6a{X_*f zSx*TQd)=xt2>TT4w7R;Q9$2-uDp{iblg@+tc>#G+pHtgX&iveYjpxFKAk4C5_c_}kDqiyVSZ}ABF zXBOW_C7dwF^It_;gc$xg@i{`-c^yg75C>?7E|}>z(sfC{{SywH3j}n3>{OWvB>Eg?v zo8E66Wj?Bovk$8^xg*!Cdxc8Kt5Z?=Z32{Y?~A!MKG#_M%*ngcW)rzAl+EGdP?M39 zcUX-V;J*ekZ7(M`AOkPqniL)VTFF?9Zx@0GLa%9YPp|-(2%dP#*`s8)fD+6Z!Ml_9 zXLgnYmGC}xlP1VMPe~^j$;7WXniE^MV$Nf@Bo*v~g>n9?kVDL%RQF3w{Zj>_v1hjN z!_Vh1-DE-cr13iUBVuEzpkZZY1ivk9D+A-Bs=mQJk(gyc!GqUZRZhw0% zb&Hei1H4h21KcfTZ2ay1(*Ak!Yvyg-fC&*cdWZ)7W2=B)CE z2L~(lYHW2zN}53$28BWZAP7(2(2$L}vGIx<@Og&O7+U$ngsRMld1SFG`gKwzLk*ZV3wAaj=mZtP@q*fKi-}?`U@V* ze5=GY1WXW^28ul?EAB-galK|`EjcJFD`S&;p;c*){b?;~>0JGIS!{6S3#XZzUqHZS z#zR*i^FIy=hVoE3-Zwin6ZxFGUS2~hQQ)Nt`cji6#zqe*7GDF^Zs5B= znmnRGD3!Iaan38eUvdf~()>{$SaG$C;O@3}c0MTv6)FQHt5tB(^c25vtTw=5?}dbf zj48;=JIQ<$Aj8T04>iT~lQpHVR5)(fyE<(F(BA5ddLo2nvkxD$;!R;1yje;3wEP4V zZ;DEYjEe3GDg3c7IZt#%a+Z4C*@dP&R2C_NHPSY+aT0w)ST;rExgiho`U?!Z&GfU z^H;N{yKM^ri}v|Ad`C94*8ZLG?-Rrxrna^U9VVIEK>_4>dDLG(*1uEQ9NH>9Bpkca z#XiMcD3~sTLh*XM;d$9Z>77oIx&p#l?0#xP!{r0edWv17H4rn+b@7}{z|G*R zVoE*pqhQ$Bn=R23adKWRr?AIO5ps7v2bqmh0&CF}4>39`%Si3Tix&dTk8~Z75Ig-} zzQ3cMr;}wyii#SI{p#- z0A{99ztuL4a)>UxhcHOE4peg=n5G^=p8t+BWp^EYxq;MvfQDvvQ)_-Su!vYaIqWYx z26n%Ik&#iyPK~+*E>{<*Xca)$Xa?MvWIv@dXUr|Y6jK!ISf9YOdHseYJUUHAqrusR zz(BFf;>5T0-&(RAi-na{mH~bPylQ%T`?k&uYTI+~n=7ojj9kzstey>&t6q5Ocal?Q zwoR@a3Z}GS&p8?f%kxEdJ4pPv}TS+>Q3hP79T?zf|(J{%NAIAa#^hypTcyK4K=$$7b??L6)Vk40P z*n6%1J7S*YkOi0W!Et?Qb#;UTyzUTIcHK8|QdtiuqWMTE^)4`6-+~e4(c$vLoz*93 zFb0&A1dYcbRpFrQ?|vRwRrmICNQxMJRSa^$XuASRQfB(%)xWJ07LamGy#M{Br8Ez2 zKRobUOxDHf2H`T!>wGb=5L)UPlSR-ok~J#;tEue=%ScK-``BDlz&txTFpEB;WyN@i zb~;P>u>z-fl?-7AG(Ut4G-~h0+Wp5Fxe@#NZDvHL?(Xh;jD637j2>oi0VEV{_6~Ie zlX7(Q#BV;N!<4P0v=Law8RQeWef5v93%DRNVMdg?KVXb%aeM+o6$e;kQ_v`F7($to z9?QOhVNC-?w&G#mEGa97*J><^2rqqN;?Y+nePJh$v>?|sx zyu%i5Kn_;O!$)L%T?tvfHbvLqfTFGsTYLTJnkMd=0fD>w=4gFHMw^VJC4sJ1^3lCi zsZfGc-*8;<+)?(|2I`&wb4{|z4Vy8Su8lORJ?!oG7odR1t~bAz#28Q#@cZ(KA-6q= zlT|fmeGbZCWqQ#*O9ma22t9p%6WmWS&N{#t=2RiwiW`%3`@i{HJ{*~jfO_dT&5#3~ z=sUE6`p^XWM)7nQlTV{DIo*ftem?XL!qVfbO}{&mhe7MpE#9{tKm12@%wg=H`>P@qRq%g9MJETU>^zCp^R|uYBu^;f{UxD^e0ZLhF zz^`&Z>(9|aIgj7>gqJm5{Z9I$jo1?+vH{jG7?frbpaqxbNnA~nGj#+2i=CB0MWu+ zNIBh`zr`3R*One@pfE;!ZR%%m08fjsjim<0tpJ_%W0lo4|5YJ#f4DH}oL~b?dAzwi z!wISt0|#F4X3vxpsD!5%XaRf%$}u_pGh0z7QMDjvFK-AjRSqy&cxF~7uAvby^`0Fa zZK?bweQjKkGMO%RzpadlV3q*{c|h(~r?;`1hS`|McaiRnJzRO4J5B5FM?z@hp{-}E|5S_>GCV}nkEv!$O53Pz7+rFmPi zMn%BZe^u{RWrYr^e2hfEHIt~_1*a_vtoA<9i@*I6qk1|9gyiTo(5x`ufIcgSY>J%VF-8o4g8_&eHC$R=m*XJH^~oeF z9(djS+@Co~F!I3(2Bp&B?D!bn&zV_pbm6hMW!5NXed)D=G3_rgrj=Ho`$x=7Od-!% zOH~<^*6l~r#Y&(7r=r@CA|68r`iDWr$M#5b6B0%h@W2EAeY`!G+kA&98u3Moo169GAnUmvCa6AsC$0}lOI|wDhmio8l-PU2X~3+foB5m0JwV|IEzH z2sFR;e#z>-o%cNZa{}Ogm>jBZ49@=g65_2Bl>f}G;#uLeNJdXjwxuS(eW zJbk}|tjVtV;u$heGsvTbSk2HqXv!rZKIFe2^$4_gcZPD$rUX&#O7kCCXZS&aDW!?* zM%@~1$=b3$y%wg(*Hdt(Eq@5#m3$FEU8dh3?<*wtx-RM3@e z`l)#*g-$aTqIj}W_Dp8&&cB-0dL%{uTkoeLA}!a*e%&pubea0B);?V*Qe{d#c{Gq} zyoB&ib24<3yUR7d_q&KR)PNt6qS8~|KAx(#8G3~^kYkSSNXnL5_>Lx583K&hU~dXB zn!Dr(-sCw1U{a79Fe;Ovd)9Q=PyP8rPLdfK8~dhn9zO`~H`?OcYhB*PD3(*m&nPY$ zQOi}*gdowZ^^Dpl<*X0nP zMyIgT5mhN2PC~->xIgLNsUkIn!hC~E$pye|-BxSgoiw1VvJT>}C-*Wdb^K`BE+%IH+5+r&8w_}P&jFsM#FbT0xoAHI zSY*VAgHcoxV;$}7jmcaVF>P-N>h=L9Wq9yO<#U&3A}+!Iy@?k^^es(zL2HM z{*422BH~DeGTtL#NkANzpf0R!=UxepjZH75n?UT~Y7}I%gUij#y<=!}F{oD4 zzRZ=-myky;o?wgc!ThM9pi(E=9Z3_R6faY-c!k=|-u}VQl>W(o`o5!hmL4}hFD0<{ z8b{xP{6s+^giI{FD*q|;26*wU7GNp_?W=YrMHNI*n&Ed?@87t~VWIGPK%aiSd(PVN z>x`(I{UIjBc96D~gUOdMRH^Mi$gEJav6Y&!`?S7?5|gMRf?S|` za%LvW0wA@jvZN_FHjOSxQpX}d$xQ*TKl*noi6YJCEInv zmAcqtn0}{JEz@TNLDY7R{%}{Xq-GehnO`FuwZi*Z3l8jPAGPm`d5Q)-J-yy>U7@3V zvQ+ZFAN8Q;mn`P}TaFaT337DNgQN_I@IG53)DbT`%cK%frxAhz^V3MgRmsg8z@l*r zct|()7l?p8`l(o@{!@&|^Fr+Gyds=;c1Q@m=VD@$Vl*F}KPiz_Qh@{|W(v&lD^Lc0 zB%U%51G*0-?8LUP5vg+#PIhK^U%puXCy?Y<+VrArx#q0sKh*4@lrQk!!^BU;kTr|2 zX`}@yz$gw=?~QxcQ0sYkayfnDU7JYbY=gaejF9Niiv|JSitJ(VZ-_2xYQDNVu zXW?&JT-^IVy}fkV5Spva*nKapVE<4|Luf4350X+TeSfqS1|Gf)y9?iQTKyd|IG z%_$i@ebshqx7xp#~BcS1Y5Zn3VNA%Cc z!lI&2mcQ~48ki9Cq$_ncA>LRDo!s#G#=z)@#3bqcJ ztM5w8d;QjyM9m3KT+HX2g(m#+;A9%^q-q-I+V8@pKaHjvdeb1wn{z>+`Zo^deX7QN z$l8+U{}u@qczx2#kXP z@23YCNy7FEt&*@r!eZn71FwwvT>g9(@&XR;-pO{m6A}`luR?e-c@TzqO+{6er|-y* z$K;xsO6?R|7W2(bsF+BvX`T%mboOYz6G(}Tr7@4QM6QDEoDP;g*AE|F>S)(8iM|BX za=S2)-$u|g#2P2NTFMCbH;X0x5n_L?@U}Lrw@oBfLgx*Hm{zTGAqd2h4MA1rNUb$Q z+3ugu_h^jufw3}ud}TYB$esbKP;>WX!fJzjl(6tGzC$9$`EvBHdV4v<)JYy+D+O^g zbsr7--@_o~L3b9X@eAIkVp)<$y}C_ECh}(ktCVvCIPd_7@g6_^{JFLUpLF;xgQQha zQ9mOm)+fT`DN7rY>xG);9wT}LXL6XuHU zmgvK0h{_dAT6d}G5`Rmc7e$~vuqN;-XfCy&P0Z;KIRAV3A-;C(N_Sc_(QFfPaTV>U0043OVqH z>a1eU+^m2{nIy!HN){}Hgs1iiw+I{)+#(-i#XU!#*ZyX@>hsiW(6-aFt8NXjOl0tP7LpaN}Hzg*9I&NUKEB?Gaik|^Bt|8>1CPw?4PTtCW zVRrU1{V!r-+K#-)M7)dz^Q|Mo+$U)(@G

>>Rot=4ne@#JB<_{pc2?!c?xINX-(3m}io1HgPWp(xq z*iJO~HdGKjuJwd~Ek9goo8sJD-ftjww7^2Ab!f3Zp~u0J|0!f}S`0N#soMQPYN^|q z15}?3hP`3Xl^WkDd}3o-g{!G+PQV_$%ZSWa?fYP?&oir+ru$20y`GtwkppKIrDwbh zE>g=oCntF$qBW8fq4tBNokWgGr~Q+YF1K+Ld_Oo7HcI5|3B+8b0SKi^4j6MVW-Ir3 zQjn1q*0>%lX+U^@!#gcT^osDY&w2EZ-@gOY?ipki{V+7$P5j#6T9Oz(*F+rz@dCtf84ersu zd(!LZ9mb*6iyP1a+rgaFeM=p!@-|<^o;?9KMGlOJ7?6G&;OBbyXme)HJV2iC7C~?*tD8xsgDbYcaa# z;yb+A`dx_feuBd!T`&mTx`+8<(voVU1a^qSOr}<0o7o}FQhU`K1IjkIww7;q32N|B z4@Ojcd;nEu#DWSvdO#^jcz4v90s%kzikvK!ynlE|h@tIVBRvWR#%d0WPQ5EcPrrcB zg;>Or&CbqF$E^1qJ}m|$pSVD3QU-VuzQ|r#4_R85eA>2JN6qMtD*A^Wx8${)Ui2vO(f(n%BR-qVn>C zvv3P|K}=TA!vC+Bn*@UtM3zJav;c#pyKJQVFSr^Oz@vTl-Q^@CdxzoGQYGo5?7;OS zxYDfyrLzunFVRyhBRjjuYsoO0(@5NiL}(+bih6q9KLE|@51%&!#!tB0+V}&wLkLs= zv5rzJtg%TmrkS3eb=F)-!CzAHt6*ZLm_pBf3bz93w~3`CLq0?=++gZZOwG(VV#?rs zg6J97Eg)P=3PD%Lf0Q7^F{8#HFrB ztOS9WKxj}TUI;x){WFz}9t zec4-ZeNd%=PI%U7?8V9{dP86r&P(MEuS zsEGOBtm+IyEkaxsCY;54kbbfv!3L>^F+=N@ar3Yd9cAgKs7l%>f&b$Im3*Ao zn3yq=Chx8;v`$>hzZpRYDWn_AT4A)c=`>myFEw13B&_`yej&x>$rXJ3bV4oQm*!U= zvSflb)2ix}uh$|x1K_h3*q5EYMs<2!6GWg$xWt0KK#;T?ny=8}@^b9s6?-&)Qdp2E z0i$5&cY7*EuKX9A_L4}(g0_~o>Ehf2<*66r$f~Y>8CC2dRhi#JLN1%P$l79u74fO5 zsRL|)Db9jiHGD_M?=}lR(~VDT1GnXPa8-?I2RA)8f0rTPxARxTpKfElKPY613Idmv8JoBdLznGM+VPynxr zbbEER3U@e@+KqeJW3{fnp5d+)YMBQ-_0_U7prTdborfhwqRR5FN@%dQf(rPhw;JZX zR|nCs?_vcpZ3EagA%EJ4gY!~s8zW$-Jqlk!{D8m1rTxscE2{e2e;Bu1?K}tiM2yxV zusl;J48O8qdn{Y-`s(`n4@iT5@1)lu^o}P`;_G)t{dWxV=Ul{8`~W$y3W(;is{Q3z z5{!Da=SD_YiT}GV?g$y7dHEzRdgK8UBIyT?cL50@INbjET8FGPSeJYhLCm>^87#>$ zIxryj6QU3=!K5Rdf@AH0A5VFg)3Ko1DfPa+k(z$g=@5pz=rfT-=_R2rvd~h`5dGqm z`jlNjpy2}c)HNXs^nC_EPN80tn<_UowfIBLO7V;rEp8_a9ML!*ej=DXqGRAhM&djF zaeDxlh7PU@9VVPYPx&^-H>n#M+K9)(*cfG&FdXsdCJR&;YM^Dm^Xc3BX}juW_ey$Q&S+kh5LFwslV zb^^wXwWf# z=&CqnNs&03htcEMOf*CKlRfTmMh6-eG?e&1b&k1R;N~Z&7HQ(o8Dzue0y!sV1#fS- z?L=|vH|VsCxv!F|NfZL%V_vUj;80Ssek#R&mk?dvV>Olln@O->r8ct36b&0W_ZPcRSYRGMAuUk-!F; z<>xP7G>+h{(3J$Rkvy$tXJRT;HZ)v1gSS}&n2h3{O+qA@D_Zj`G1ov7@FiTRb^i#j z(v|>^AZ_PN^B0f3{?iW%8jS}fg3Dh#dXO#bso(N^zWX^VilDV=+j-)p406iVQFi*% zT*ziXSXpl2wTwAR!OCaGw+dgreEE%M3?fLO8K+2ex)`mye|62iITEl3g&f(@*5XKRyub#{mnIxwK}IY+sdi!e56%rW(+ zw6t^@R^Bkt%_y@DEA)?W@e^A2Q!7@3S5iNFzj?fW&o?AvCld!F1b@LLARvGkz)%wW z-W3}7!582re89!WugV2XkOY&b1$K#XGPqmX6gmP-SMqT`G6nx~*38;^5FWP5-EN!vQYXq#rNz(b_4V~+8b?G8 z4%zH6O;Cd3|Tu}eh2O5a#B6~6uI?v^etDoVAsv%Bq|oxOSuVrU8Ycq|~I ztpBgQ>;9(tkN=T9a&1XgitHq#>`|zM?4*c{l2t}Bvr8x=BQvEGvVD?0i(ExkWlKg@ zg;3weJ>UP~`_uQF&gXQ_NAA7v`+hygW4&dXq%BbCtRDils(b9RwzQ{eU_<^DX;?QJeJ>g=91tY;m~0wcUV6)^zV5kyOT?`Adh{kA z_apc}7#hZCSlWo60M!*?ZutNiYXRi4v>Y599f*0TMfd!GqjAZH@6XbhW(U3ry&>_9 z#<2X6`e0dko8ew;d15kFy%OgtdNm_;jf_}!3K*NdEEYi$#ALBUH*YI0gaQmD8ZWP? zsqNt0;c9Cu^ArUwJxkN=n9PaL7d*_3Zpk~DDA~sHt#$gKcD&WDk)V0lo!<;j$H-wRqF>`6`aC@W3MM!eA zuT5ijyg|jlRZsFi;b`Rn#vGV#e&*7?s-3;FnHpul?8nSZJLJuC#`y-p%oN=t0wN-j z$EdI}&JSZXeT;kOCCp}aI9E?iMcBRooHoT6Za~uG?P(;i`&8W)df=`Z$Ro_dck<-P zW{TQEjOLfow;sb(9T+-cR2SeM1M{y`kwlL1?*F(vZmoUsrfl2E>hWl~8zfda10y3z z3U?OrTjPJIUCN`>jqZeFPSN%CkV{MOU56p?PyIb}js1M%$|yF=>LPSc>l=8((uxg+ zzGfcY4y?)6?7hJMRHA4dfSv3RpVqyiSLIavxTYq?6zuxK6lN0u# zxA!Mc;9-vzy?4Y5Tzd=^g@xOgH{Kp`?lmwevffsLwF}eQXayy&<&PMh&9OZq>}*Fz zg(U;4Q7@Rv%F7jF7|1s@<1mu!<>ajF?(Do;Ju|@RK7#~89nwol`T)!Wh$qY^}o4i5E zTJ?n8U!UaVnZTzwvkYl$2l$x*BGKRxlm-lUA({(Wf{pOG!7E#BRvM41XE#=+a)BUv zU4{6F41+eAAk)iH9^0^P;;DlN4m|ONmHZ2uhi>sbSSSY|6La{lbm@;b5TRhNcY|AS zz&Qdlr|yDsHEz0%(j3&*p7J+zvwp#tGB|584^y{9d?mMh$-v&7O$JADqvzdO6_J}{ zWRY*>KJQeUR}Vidq^LN!9~y`E5SyQarXl>0tn96Kg_c|aA~6f%_`D{nN=iKB8E=EO zZ=)(lW>xU%Sv16le6YX><$JBLJd0fsy9Rk!830$cKSWWVjL^)SHo(wrA|!UwZo{{?Y0^yeXph znSwGhti7;Q{|7&_uW+N=#YHx?p$ve(-%cMtaK{bO*u8nmH-BCvZqX*X@y}~$Y7R!R z3d=wTt@+BlQ>s_?R=;}%$*c7XjXeS@bM6HG(lwT^9>xomb>4#qAB(nLw9qMLFG)Dl zuD;}%$=IgOzP(8sWWzCUO1K%>AvSNKZBUAkp6@d=>B}oCmaB#KHZ$Sew@9XgO2;1Z zruyFic^bN|fJoY?h)kR#X5WrMZ9e2o-59^4NHUc90L`?9TayK^qqx=441++zLC+ki zBLK`&v5yHTcTa&tko^7o_cEklN)C2*2?a$(BQ=1~VgwRoWU4}jtJJNo^YrioH>(xGg ze~xWeWca6uMs0val7!TThK8fKt}7%A4iZp?w;_F9?d`;{w$zaeG;$Ug^;7VRT}1Jr zxEiAoJrpf#0tiF<$JA8VtWpd(&xZiP3LrTUxe>eWDmd^6Kl3}gNDm9;pCW`1s3m+o z@VMp~38^1;;`$UX8E#~Hi_T2Fj`@^^rJ0+XTPCV)Q*{G7kJ^oNZ{wDO`Np?H7~0>k zKOacQQwx>mt4QoOHW?3L$_b$s#iq|VF4dwfzT}HFrEdmE{B(Hdef_{(rXy=TY*qT*2Wp~-LG9qDLo zeWItPruGxRuxWfB>rQp*?n-`u6Q>Ht-V%U%KZhSQgass@{S4dnsk6itXXSVq4&u43&9Er`o{Sfk+njw;de^>#!C1 zk&=57i@4tZDrHt$B!sxtBZTDwjl9OWzTYFaR=bvg_sj_Vsc&kU*!_am{=4t#1mJ&} zJ7iKA$jQI9kqPUk4~RbKxU6xJjpKNAb#?l_tk4#LbZSfCR!2>wpZ!@v&`KISX%{n+ zk`DYl6kfLOIW1T-xcKMa`AD&08cly2kr$=~)mIqD#FzN@74Olk2Zq)GP9j%MS~?B` z!E7-IY~-U!Fw;FqOq@CxuX8WSgg?5`oZH^s{s^JOnSW+OK(*2zjb;`Xsk5LUH}@k) z^+)hQMS%EE$DPYaPrs|sSOdP(aa8M7Nl7&J^}>1V?y>gcWKDd0KiG!~Eni+{E}2|h zRIMixmt+F1#tmH`%c_D^$6ZS z;c%O;YWzmW$i`J8h_)*z3e<$S?Ye*4izMlXl7X=a6g#?-$(}{+R%*L%ZasM%@J7Qy z{;xl2XIzAK>vV%#3SZK{h1nyww$-n3i5L2dT$K->#~Ps?Y+ix&u>qQ7)mmWsyTSE% zX(!iVOsow*E_;23D4*{=GdFiErO2yEKTU3dt&Xo`&1o4P-JVRF2UDlXTCS;f?zFVD z)GMQ0|GT+ZL_&9X-j!S`Rt`&k1L~z{BQ?TNVCmD!`b0hU?t}E~L5Y{rkbzjfL0+kC z()R2I2=O9l7O5eOqHS`0KPm0pD`EnRl0oN*#A|;oNM|QbFVBMbVr6yJ1bZg8qoLb% zwof0LV`jBeB((({302I@L;K=&lu&!#L%cPehED(Kq@Es2YodM6-_W4N5H?jqs~iTh zk@i6ev%>WMpj6SN3mZ2FT6K7?_u1-WTqI5*7TOZ|uPgbxfDIYKHFzGtV^UM_{me|h zDw1B>BMd;}P!5Bt-h8s0cXB4vtJEoE{d`%(_{P9vq+NcJ(09!)tD$j@mp~YWBSckq zmK2ZF9w+eSsY^>xpplI26jHXLryGsU$6b_xl9G+xFkpU=Teu;ECRCu5zlE<-l1ADfptFL4H*fr{a7$i1^#G(=1j#`$iEs8byx;>QDY7GFantbdo;}94 zI?PI?M4H&W$K(!*h_rLpF}ti?_fLYdOprBAk{8;u<#*S>L9G=kz$^KTkGyN zI-OzB{MR)TxP^cGyj-8@@G?JNBo=$i#Yo2?q+-x?=9o6?s$CyuZM;ah?qXxpkoW`n zp`CI^j>J4S%x#UqIunL8O+NV}N5aC>_$`IQQ~wwm8u|l+G8w}cY=o>=bx;bQ7@3&N z!1umu1ApTHdEqZ0ZEj0O{uNfO{z0Xe;l|Gy2f=3-vQ7<;Nt!?Dkh zK`dq}`?ZFNEmvq>qUPngX8lRyN26()k%S~5q^IFGbO&Ns4(p-y712wuh|W|izmIa z+AdxtbH_(|jY}7TD8^4{If`C@nbXlKU6^${=#ZTxosSaGxrdg<>V`@7FM(EzLU&+t z7CNI^*vj&6A=1F>ZNkAH_}FqF)+qNq=y;p!OwU7gE9i#04K*2styzI7BPFH5fckmJ zy{Lf*B;y46UBdTvhJk^Bm!?Usc?=$OGx*KwdN5Z%K?f+ag;Bp8oxThkLW2^!+p6YH zjhbA-g!wb0{j!nVwGh=$4`d)=^07u5kNh>B`NuN!^mm3aZw}rB=GQ}tc;P~JQWreQ zLgD{xW6T(?UI9H2A?GL;u(kdL)2SBi@yA*oshm71N|{oy0PBYCZpXXjO2FHEhx>-ozj zVFDtC1^Q`gT;;%jP+#_Z+pk3!%K6}s@MuL!oqU;fM5^paQ;#o#G+)14Tp5@A!(=R)2YJke9ZFvq zWQW`YZs>wTbtQPb>ej*ueIoQ+p4Q*tL+*@3xC-(T{)ochIR{ZFHPaQt3N7oyq0+MF zOYAyQ;Cq$RdO@!zJMO3Q^c)qV8I(hCRJ&Lr+C}iW>C4#~gm_v&7Ip19%FqVhFL!uY zSl?^6F>-Guey)uSNlP?+MganNG{Z;v?W-c`PqBM6gdU3KqoZ$AyN3PVr1bJ>ZT!AQ zg;(qc{K>7&AF{#UQTc5&|X5 zsNCI%3jAvHjAIn)h8KMzMp^x(q$EGZt^H@~nB)ssi)1F*cDZ`Bb{4d>HUy`KJDD0H zjE~W%^eoaBu?}Cz3X57(Uq1~iD>4=eOx<|x92`!%0{j%D=7w|T;PZ>CHv27neSH@I zguTc6eR~-$#nlY@)?BG)&}7A<>DG=tG&-D9C45)U6l-YxYwW>LAe22E|8{x=* zavC(0qJ?G7ebId%KOPIfVX>ZBK4+D$ll`?#wCww&uwC-jLPajkhWaJpojY&a_S5@u zeClv0W4o4UIWIJS23a+poD!DTtBlQM-%v0>}K!_f@R=pUc(|2=PU21Q=DU6eVU3;SKr zCySGxZ^}X{rXpkE##`Lw126k{jR~OVQsSNJ{Ng=-xw6!#hcVFbz?nBeS}B=kN{!Y( zU)tRG{q!+8OX&KOGbc}as#}_Yi_kbqeG9X1s@eemlUE5kn5W6jxMbZ&Pq;7UY z7HZ}A>BVk8c=0LMw_rFbHDLRF-aQghvEbIBnxzW*?Ic|q7A^nSn*r{YFV@^|vN(g4Omaxbb7h9$X|;nl&$%pq#A8;0>SH(%hMv?SUb+-#qDM zkS|fn;wP))B};(R8fSfJW{DaQm0j7R zM*nVor(w=1-946S!*%@L68|YT+blIojT^nrzdW&6F_a0lcXyABfX4p`1iJS5xym0Y zfF^!KM@AOE@m}O5e`46NsgFJQq>2bPrMN6whXbkM;4y$NyTjncaRwan@4dkgoKN!>@dexj1rH3JJcU5vgl z`nv=UOnO{#a?-`DX-s19NHWCC{VW|41H!1KvXQD<;ksB$_bZs-cbbp+uq9JV?vVNi z^e6i7{q@HrIwNQ-t-Sn*?KHnu^UHhhj9s22%-t;5FwW)`AHTsqFbg7CN?V;Oq5s5t zR}=#iCnqOic!TnxhHXfLc5t3s#*T)r%eKLrzVgj2=<|~k8 z;zAPeKSCfGy;32;2#|I(G<>3i=VY9a+^5sQPo}kt?RhA@^DZt0;@R@6rjx zExm(bcp3uw-@kr*@hh*c-Z-DF_ZxK1ZD0{M6YVX8jng@DjYXH$?gNnA?SCl)c5-Kd z;*1RyV1psQqK?kG7qs!AjSS+; z$bp-!3uFB&v5hAEEBfQzdE)E!q@C^Fuv4cndRIYpFT76OfD`sj{rAGWc=~2y~u_J_u zoZ$NrTUT&(wW7u;8O*uA;r22BRn6HMK>I3a(fm#4;9v@~ylG=|<)Km2yRCX6Enx$V zkdt-cZ7x^K9y@lf8f3~h1ZK^rD0)6w#ZvTo3!ByuZmv03N5>go048(5v7RByHJ_=f ztEXpq6o;%T5n?&_K|u2V2_BCtT5PxPALX*Mv&%TVC+0;cl zn*p7&O$75Zh41IG^l8{XY|Tgl^d-(4;B{(AVkTIsER^Nt` zcPMiA{=aF!Q?;a}rHNU^&R$->??89-lV9EDEKOWxxN~vv@(n7=U}08z6&fnLTD2LF zo9^l(Gu!lIg7G?qP|Rr!4G({VAjuzq!n(L+y6`*iPCxqQ8Ac4*&(6P=+rz~bnemp` z**inM&%mflL)HtKmy^G>@koJ|wl=YVjj7~ooRU^tX5hUG?*$?O@dx%%Fgr8j_Gm4uy8ckn4x9nkrEa2|&2&?&*0F!M5)Me6C}FypI$ilmh~d-kKstqi#`LkY+8 z1WOd_0+KuABe4@Yn&zD1P$Z>`eqOHt=&U(iNx?_Kvv@Gz>9#)MUd^Xh z4d;GJr|`hb^V=I7x$ee?z1`d#z0erc=_R}#?buwIq}`Hj{)Q;`Sen459J-1`fawe- zj)L7L_G5LN-37?X>wS*TF^g^DO5th32xM+5fNW z{A2A6>IR?^Q&BUhqiDo!CE$toS(>v!fkO5BV*g+_v>0^#)VAFOFIV&Ws z=yJbO+oK5b(EF(9I!I2EjEGQ_l444%`?5Zwoja;&b2gldTPCoU#2zmlbj)X1ki{C8 zixst?{fdOPsduYC)oI-S{aS}NV3?2wT4c3MmVL7#>pMKKPSVZW33L>AY(>bNG9z8f zvU{TU64elQSwTWVLI`=KpaSFI$jFE)@=d=(NLSXglE4%xneS_ETI9;ST7Z~qJFF^0 zHvxczLOkt{Qce2^(TzXN)bn`RYE4%3i|Ph^XhVFMLd#0fCEtHLX+0%yHn%F>?s3!D z>y8J)ZFe|Ubw}=MyqBaP_3X?$rHauh2|8mWbME4}+)HwePt-ij>I9nSH^$dx$JR7T?%nIe3jt>IOIq7OGnVL+fd$+i6W3X1YtR1{fH`IYH_v ze{1#EuWhUMd>G6+TMBFFL%Y@JKlSve7uiif6EN=?Ogym;0Z^0lH&6>hB%8ydB&iDl z*%&#D0LD8;rMrng`=+a#7Hpr6CFf5BUw~R>G7U79LZDex*QPkLX`q3_#=3Ph+>?klPc+!`n zP5%gOy%lk3>^|pJ?3*iOd#S}+w$iEP6%}_6v4?E`mB|RJOp!_on()Lu-m!NVpUUSO zV~t@BnC-*HU#2^7rDjwOTytXP5r>T6DxOhtVp0JGW3t0erZC~5M)v5R({(##X&(gq z>NB${PeZTt|6r-KEN7>=RpsV&NE+F*HfW|AM-sF11-F6dNCVLk0_20N0)ujAlh7V% z?4d`)gPa-!_H8HyxoxbTrrkw=NLjOR%iMjHHp79Y6^h$ZD4}W8!itdbQ;EcrFX_Cog z%z`SXF{;?DyK(j(NA#%2#(@+%&4X^i-+Qb|T!h3sW_D)mLXlLhZ9;~?S$4>qU^Ubdo z-j(~ZXTGkU)|~Dp;7F3w917ps+}xtDX=h{{vr=<6G`8JH4;_Q&MX>`$ouMxnWfF=i zy27e=2&fbj2?-#TE&@<$Ph5?Tjg>NtTM1#l!BIWYhq<_v)Wy+wpY%Xm_j-C}CelWB zmkhPl1rNIIzW38V%qcyfT^2;yVk8jymU3h_2y_c6YmEmdElBPr`F{>?{ z?v17jr;P{|rP%sF135p%#*Qo3-N$(Z9!`2F_HVy$Yz!$#se(J(sB&bIGWR&)E-<*S z6&SPMlQJDtFb+Qj`xbIhi}(7g5JMs%-kqG3ZhqpKF~?^y``rPonVhnAf1B5(D$?z^ zj^E+C+-T8Et6)9)PSoUia|;1PF}v)u#!Pe`Hybf+MN>TuEG#a?9jR6teo^ibQ7c@3UH><-&pXQybUCUE9znctc9YquEY$l_E1J;j>~RdUZ1pIeZmq`$gYxh^u}{BQI84bfY^a1rh{Bq zk(eAh=o8E!V;{$&>HsJI&5WZr^rTcWPEu%i*H`{v{MjgZhjH1JDuR`dmh?JwdQlCM zh-f|R4f2Zq&28V*M1DV+SlLGG8Ky(Mg;PXJP`lfNW8QJ$LkV%G?o;H@W!Tz zYVuY==&TYSOMGkOWg`YDuC`+F*Eg}`oP`Hp6^xzU)S;rVu$FU8muo}3US$>Trx0D< zP{+%*1&2opQvWYH$G8rkS{*p6Qj(1{5(S4d;fsbUCuWN|7|K*6;&sNC(gKI(B*iZX zaUDwMgTyiwHQ#~89$tIJ|9(V8o78xw5D$dK?HykYp%7dy3TkAi3G&08b$s-2lKpAu zPl(OY&=+Xw@zkDCLW!&kFpVbM6<~>?c+iZ_6kU;{vBO`NJU;g6R&qu9dR*L8^14}= zAO&5?wI$UiX+{gGLX$N!dKfAzN=pIcn+XgbcXD)G z*WV{#OW5$DZFZniZjB6NZ}3_x{3>#V;K}ZN_@P$8G%^tv(*-k1-4ZR z1xfGsUUi_N)e7;UCJ>RnN>p!%LXq&?CmN=)To@o~q+mS1uweKyVxcFOI`QwHyEr-o zZGN8wAR`Xo3Vna!wj{-vr`DVOGp#+zpX0FxSpQ|uQ8KTywF7trUGq0uzGG1ez*_a+&g~ztUU97hJAfdG`a4KLEeR{)&esnnL|n#nM-|Ug5)E5PH6k zM$_kE%-y@@uovCuHW@QH3l)kNB4*q{$;{SEJveTMe~=ML{^~U7x4?M-nd;tZWucPK zHFHL}K79BvK0UE%S2rdWbokaadD0t-sG7TgfKaf&Ypk^{;>oNrOFV38Q|GdtuSXS( z#?Xem0+KwB-O}OGZrSU9%d(4igou!3Gvjeq6wO6pKIBb{%ii2;9^(ThJ{&8wYJzk0+5$pTw z5xdKmEdhaFcy1ta50UQt@M!TY(CMc literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..ecaddfec937aff69b99281b79835fc9d4d640267 GIT binary patch literal 8357 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..587156486b46a7f46b4ba2bd5a92968bb1933f95 GIT binary patch literal 12236 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 literal 0 HcmV?d00001 diff --git a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..587156486b46a7f46b4ba2bd5a92968bb1933f95 GIT binary patch literal 12236 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 + + #FFFFFF + \ No newline at end of file diff --git a/frontend/android/app/src/main/res/values/strings.xml b/frontend/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..faa7838 --- /dev/null +++ b/frontend/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Benchpress Secret + Benchpress Secret + com.benchpress.secret + com.benchpress.secret + diff --git a/frontend/android/app/src/main/res/values/styles.xml b/frontend/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d27b388 --- /dev/null +++ b/frontend/android/app/src/main/res/values/styles.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/android/app/src/main/res/xml/file_paths.xml b/frontend/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/frontend/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/android/app/src/main/res/xml/network_security_config.xml b/frontend/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..8e5a458 --- /dev/null +++ b/frontend/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + 10.0.2.2 + localhost + 127.0.0.1 + + diff --git a/frontend/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/frontend/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/frontend/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/frontend/android/build-apk.ps1 b/frontend/android/build-apk.ps1 new file mode 100644 index 0000000..bfd98a7 --- /dev/null +++ b/frontend/android/build-apk.ps1 @@ -0,0 +1,54 @@ +# Builds a debug APK with a JDK that Gradle 8.14 / AGP 8.13 can run. +# The Java on PATH is Java 8. Android Studio's bundled JBR is Java 25, +# which this Gradle version does not support. Temurin 21 is the right runtime. + +$ErrorActionPreference = "Stop" +Set-Location $PSScriptRoot + +function Get-JavaMajor([string]$JdkDir) { + $release = Join-Path $JdkDir "release" + if (-not (Test-Path $release)) { return 0 } + $line = Select-String -Path $release -Pattern 'JAVA_VERSION="(\d+)' | Select-Object -First 1 + if (-not $line) { return 0 } + return [int]$line.Matches[0].Groups[1].Value +} + +function Find-Jdk { + $candidates = @( + "$env:USERPROFILE\.jdks\temurin-21", + "$env:USERPROFILE\.jdks\temurin-17", + ${env:JAVA_HOME} + ) | Where-Object { $_ } + + foreach ($dir in $candidates) { + $java = Join-Path $dir "bin\java.exe" + if (-not (Test-Path $java)) { continue } + $major = Get-JavaMajor $dir + if ($major -ge 17 -and $major -le 24) { + return $dir + } + } + return $null +} + +$jdk = Find-Jdk +if (-not $jdk) { + Write-Error @" +No JDK 17-21 found. +Install Temurin 21, or point JAVA_HOME at one. +Android Studio's bundled JBR is Java 25; Gradle 8.14.3 cannot use it. +The Java on PATH is Java 8; AGP 8.13 cannot use it either. +"@ +} + +$env:JAVA_HOME = $jdk +$env:PATH = "$(Join-Path $jdk 'bin');$env:PATH" + +Write-Host "JAVA_HOME=$env:JAVA_HOME" +& (Join-Path $PSScriptRoot "gradlew.bat") assembleDebug @args +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +$apk = Join-Path $PSScriptRoot "app\build\outputs\apk\debug\app-debug.apk" +if (Test-Path $apk) { + Write-Host "APK: $apk" +} diff --git a/frontend/android/build.gradle b/frontend/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/frontend/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/frontend/android/capacitor.settings.gradle b/frontend/android/capacitor.settings.gradle new file mode 100644 index 0000000..9a5fa87 --- /dev/null +++ b/frontend/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') diff --git a/frontend/android/gradle.properties b/frontend/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/frontend/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/frontend/android/gradle/wrapper/gradle-wrapper.jar b/frontend/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/frontend/android/gradlew.bat b/frontend/android/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/frontend/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/frontend/android/settings.gradle b/frontend/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/frontend/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/frontend/android/variables.gradle b/frontend/android/variables.gradle new file mode 100644 index 0000000..ee4ba41 --- /dev/null +++ b/frontend/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/frontend/capacitor.config.ts b/frontend/capacitor.config.ts new file mode 100644 index 0000000..6fb9c3f --- /dev/null +++ b/frontend/capacitor.config.ts @@ -0,0 +1,15 @@ +import type { CapacitorConfig } from "@capacitor/cli"; + +const config: CapacitorConfig = { + appId: "com.benchpress.secret", + appName: "Benchpress Secret", + webDir: "dist", + server: { + androidScheme: "https", + }, + android: { + allowMixedContent: true, + }, +}; + +export default config; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7963d73 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Benchpress Secret + + +

+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..d8d079c --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ffc2ce5 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2617 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@capacitor/android": "^8.5.2", + "@capacitor/core": "^8.5.2", + "dexie": "^4.4.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" + }, + "devDependencies": { + "@capacitor/cli": "^8.5.2", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } + }, + "node_modules/@capacitor/android": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.5.2.tgz", + "integrity": "sha512-rtiaz78egCcS+vs29wkWCL3C+RhazKMSygXcjutwWP8NmO6r5UBxOvGf3Wjz5+ahobyQ723tn9gCePwRwcNDZQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.5.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz", + "integrity": "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xcode": "^3.0.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz", + "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.87.0.tgz", + "integrity": "sha512-22tVIHlB9898OYsBKmYB6He1MCSqdTmK1BVWuSNUTVrpS0KWNege+U9uEYqGJ+KatQag0r/oU44i3OOs46z7HQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.87.0.tgz", + "integrity": "sha512-JfZQ3MWf74n8ov0DojZpkOduIubYJvZL5tbTMMhDutsal6u1xH1QuNpN+lK+EQwF8AeAfgwTR2fgh5U2br27XA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.87.0.tgz", + "integrity": "sha512-rkx6Xvvw93z2GImVOyvbZwAZcoY0Z6MJW177XNsSB+i/CBsH7ANyelTp01VZyIkM3iSXtqKVb4+ptBgPHap5pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.87.0.tgz", + "integrity": "sha512-cw5nYw7L/pQ2SgGBgOcg/Qo89llQre3vIc/kuBLhSICzu1r5s8L76H3ug6AIFsD98WpROj8/z5JVzkf4daxHaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.87.0.tgz", + "integrity": "sha512-x/SuznE6loRZK9gz9AS4ZmzNzSu+5KdeoCS+sVHhnrE8HM6AD8UyTjwldQTTqlXGvkwHASgB0NiTNOc3o3PdAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.87.0.tgz", + "integrity": "sha512-+JEDmCAXiiFmKhAjZhgAOwaHWY6WENOQ5QA+lFuY59tgsUCDOTRi46B+lENK6CWvtxsXpwkZXNOgpjAQcpIpnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.87.0.tgz", + "integrity": "sha512-/uC9XqXLAC9AHFLNycwmSQEvKqGrOCgU+uXALI7gNW0DG81TJ/3T2eHnKdRvcb5R1gFEfjxDwI4e4zIa29rICQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.87.0.tgz", + "integrity": "sha512-FTRYPsBy/M728vnUv7l+KAwSAa3Qi4BQbhVvy7NYuMTZ0sUa/OWsbMx79HVFw6u/5KWcaYayDsoOiv7xbevzDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.87.0.tgz", + "integrity": "sha512-6sC+N7IY0h1egd8SrYUGNN7CUyKJZ4bIZakKa7V7hfinf4jfi8Kq4+klJvFp5DqXcwMn44I5MeBwmJluO1odFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.87.0.tgz", + "integrity": "sha512-d5Mnn+ICbhoJPi37Ccjy+86T5qfH2+mSTrTMBXNPfB+wqSWOayBBLLIrCXdUy1YssluCVdGkl+DO9w5r5ODmUA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.87.0.tgz", + "integrity": "sha512-sf5YWuUo1Dx9loqWCgF8P278MycfcD4JHjDC/LV5j11L3Lqa4PFiwCjD6B8kP4AjsgZZkLn3xbpJEtZBaccs4Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.87.0.tgz", + "integrity": "sha512-+aCraYbmK3uU3P0rFojjeaK9pGY6JvBDXh/0CM+gPyWKmNmXmnFe0qj4qj02U4ibFpbd9Pa9CmDvIdjJUd29rA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.87.0.tgz", + "integrity": "sha512-7rzbZQIljo6VLWVPqUyLa2P/WV3VFsJB4M52R4NCUJn1A50yjtUsHxiMHFL9opEKGFIiTbZ3VQzmtcJOPUq4NQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.87.0.tgz", + "integrity": "sha512-Dry46Fa+AXE/xt9J/YriujU1pU9mzfNWHGQwtOYibAFH8HLrKWCi93AhIGnuUMiwn2JbuBkRyz51f50Ly4r4ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.87.0.tgz", + "integrity": "sha512-f/HMZez74gJvnRpGz+b5sdGyWrEgRLmFTXg9sNoqjPWogGxYHUhXKTKyMiYsu9rUGR2ciYqL3fk+Ua4JhMJbRA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.87.0.tgz", + "integrity": "sha512-lEC6ZfPZm/07JlB7FsOWX9o1RiHlXiBW594LSUGuXmekC8LZmvUPP9XzE2WZcNxJi5cHA/Fw56o02c3JxgMA5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.87.0.tgz", + "integrity": "sha512-afBWKOD36dUiA9AenaiWpCg8xfyUVPYznyAAVXu413brK4RA5UI/Xoc4yfeGIT8G1a44MV+JR7hioryerBDzGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.87.0.tgz", + "integrity": "sha512-daptNsEOuru4/nJTxV4DBHNJ5GepzmaBVQf+jjgKQGi9NRlZKik+RFq4hUL7ewsF1pwjKM+SkYaMbF82UwFGnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.87.0.tgz", + "integrity": "sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "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.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "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.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "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.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "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.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "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.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "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.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/dexie": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.6.tgz", + "integrity": "sha512-hJP/BO6mjB+tX6hToIO1kmxYLNmun90wYbfcoAoLpKEyDYal/k33dg0TAfoUe2TDsbbLoVIzljJ3BN2UbR5EOg==", + "license": "Apache-2.0" + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "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/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "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.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "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/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oxlint": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.87.0.tgz", + "integrity": "sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.87.0", + "@oxlint/binding-android-arm64": "1.87.0", + "@oxlint/binding-darwin-arm64": "1.87.0", + "@oxlint/binding-darwin-x64": "1.87.0", + "@oxlint/binding-freebsd-x64": "1.87.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.87.0", + "@oxlint/binding-linux-arm-musleabihf": "1.87.0", + "@oxlint/binding-linux-arm64-gnu": "1.87.0", + "@oxlint/binding-linux-arm64-musl": "1.87.0", + "@oxlint/binding-linux-ppc64-gnu": "1.87.0", + "@oxlint/binding-linux-riscv64-gnu": "1.87.0", + "@oxlint/binding-linux-riscv64-musl": "1.87.0", + "@oxlint/binding-linux-s390x-gnu": "1.87.0", + "@oxlint/binding-linux-x64-gnu": "1.87.0", + "@oxlint/binding-linux-x64-musl": "1.87.0", + "@oxlint/binding-openharmony-arm64": "1.87.0", + "@oxlint/binding-win32-arm64-msvc": "1.87.0", + "@oxlint/binding-win32-ia32-msvc": "1.87.0", + "@oxlint/binding-win32-x64-msvc": "1.87.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2003", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-router": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "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.7.1", + "esbuild": "^0.27.0 || ^0.28.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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9d28bd6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "build:android": "tsc -b && vite build --mode android", + "lint": "oxlint", + "preview": "vite preview", + "cap:sync": "npm run build:android && npx cap sync android", + "cap:open": "npx cap open android" + }, + "dependencies": { + "@capacitor/android": "^8.5.2", + "@capacitor/core": "^8.5.2", + "dexie": "^4.4.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" + }, + "devDependencies": { + "@capacitor/cli": "^8.5.2", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } +} diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..529a07b139cd71d500f1ae1741e8a8de9c878c14 GIT binary patch literal 1214 zcmV;v1VQ_WP)hQ zMN{q~_c1fM%rMT^Io~(a@c(>$-^zdCS8wn0KJW9M^Lu{J@0?eFhzMT@;a4XACh)Dm z|4RX9HJ+TDfI^`FZ*Ont>FMFQmzNiKdwb)H);Syw9y{;u7tIlzW0T1Qot>S~+S&>_ zoepkpZd|#Xi5V|1FZlZUie|GJ`}_N81!iYwqt$A~e}Ua@$IQ%3)a&*9JUcs!Hk%DK z8VycOO`%q+eYalZBq9?194;*_!NtV|q^GCD&dv^uj*dcJULG7B9f6OJ5AVuuw}Y3L z7pr8kSisZMlfQj+S*$5^72BNOonQ;8W$HAdH(S55GN)kaD9FK-4VdY zGrYUI<3#D{>Fk4sh6eV=_VzZ4#bRt~YC?y@ffpASynb?W5)}#sK0G|&+uIw@QKG`a zLL3_#LxaJ9gM)+E(b0kP^YcuWzW)yahr_|w)6)~Sx3{BQE=P$(f;BZYY|zKYM~se+ zMu9-Ufk-U^CC$mnL6u7NE-+zfX$hB?m+|=cm=!dcOs;R75W4D7C|$bKK&e!ML?VIs_;?5m z4CGywm6dUMlJg@XBH-ZQ0BklJH=%@t1c;4|<*JE_isH(0Cn5YD0aAi4>VQ;cv>Q@_ zf>9Y!PoSfrK0ySD90eX383C0_#eHIJZH-A1`IVIwUPGR9m;X}*obdDWgWKC%=<4d? zelR{h4j~~Sd^B@&b1X0Ug~;sf?eTfj5mB=4?r!!8?UsmBBl>3oK0ZDixW2v~q*5uX z5f>K+k&%(?lbM+reown45@~5^kdl%D&(F``>+1^z1qHm`UD=Ni{fPicO#w-MGS)`} zRaaMoOeTZe+*~LwE@rjJJ9Tw+e5Zuu4cfW0)&CU%4-XH%H#awPQn6Uf&lHFZN`c7> zsi~=a_M{B8S1L*RgOWltPadLJH2xI$7oY~?-aKeb-SPVRnwtxaov>Ie+zF*psq4@8 zu|c^*_@^WI)hS&~$_X0N&ZrEjRB24>XvcK+RPSWqt*tFroX;TOe*A?Her57+0^bUJ cD?keT00eK*nuib!RR91007*qoM6N<$f<@*>Gynhq literal 0 HcmV?d00001 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/scripts/build-apk.ps1 b/frontend/scripts/build-apk.ps1 new file mode 100644 index 0000000..3cf1f65 --- /dev/null +++ b/frontend/scripts/build-apk.ps1 @@ -0,0 +1,7 @@ +# Собирает debug APK. JDK подставляет android\build-apk.ps1 (Temurin 21). +# Запуск: powershell -ExecutionPolicy Bypass -File C:\Dev\Benchpress\frontend\scripts\build-apk.ps1 + +$ErrorActionPreference = "Stop" +$gradleScript = Join-Path $PSScriptRoot "..\android\build-apk.ps1" +& $gradleScript @args +exit $LASTEXITCODE diff --git a/frontend/scripts/install-emulator.ps1 b/frontend/scripts/install-emulator.ps1 new file mode 100644 index 0000000..d93bc1b --- /dev/null +++ b/frontend/scripts/install-emulator.ps1 @@ -0,0 +1,65 @@ +# Ставит собранный debug APK на эмулятор и открывает приложение. +# Если виртуалка выключена, запускает первый AVD и ждёт загрузки. +# Запуск: powershell -ExecutionPolicy Bypass -File C:\Dev\Benchpress\frontend\scripts\install-emulator.ps1 + +$ErrorActionPreference = "Stop" + +$sdk = Join-Path $env:LOCALAPPDATA "Android\Sdk" +$adb = Join-Path $sdk "platform-tools\adb.exe" +$emulator = Join-Path $sdk "emulator\emulator.exe" +$apk = Resolve-Path (Join-Path $PSScriptRoot "..\android\app\build\outputs\apk\debug\app-debug.apk") -ErrorAction SilentlyContinue +$package = "com.benchpress.secret" + +if (-not (Test-Path $adb)) { + Write-Error "adb не найден: $adb" +} +if (-not $apk) { + Write-Error "APK ещё нет. Сначала запусти scripts\build-apk.ps1" +} + +function Get-EmulatorSerial { + $lines = & $adb devices + foreach ($line in $lines) { + if ($line -match "^(emulator-\d+)\s+device\s*$") { + return $Matches[1] + } + } + return $null +} + +$serial = Get-EmulatorSerial +if (-not $serial) { + if (-not (Test-Path $emulator)) { + Write-Error "Эмулятор не найден: $emulator" + } + $avds = @(& $emulator -list-avds | Where-Object { $_ -and $_.Trim() }) + if ($avds.Count -eq 0) { + Write-Error "Нет виртуальных устройств. Создай его в Android Studio: Device Manager." + } + $avd = $avds[0].Trim() + Write-Host "Запуск эмулятора $avd ..." + Start-Process -FilePath $emulator -ArgumentList @("-avd", $avd) + & $adb wait-for-device + $deadline = (Get-Date).AddMinutes(4) + do { + Start-Sleep -Seconds 2 + $serial = Get-EmulatorSerial + if (-not $serial) { continue } + $boot = (& $adb -s $serial shell getprop sys.boot_completed | Out-String).Trim() + if ($boot -eq "1") { break } + } while ((Get-Date) -lt $deadline) + $serial = Get-EmulatorSerial + if (-not $serial) { + Write-Error "Эмулятор не успел загрузиться." + } +} + +Write-Host "Установка на $serial ..." +& $adb -s $serial install -r $apk +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +& $adb -s $serial shell am force-stop $package +& $adb -s $serial shell am start -n "$package/.MainActivity" +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "Приложение открыто на $serial" diff --git a/frontend/scripts/update-app.ps1 b/frontend/scripts/update-app.ps1 new file mode 100644 index 0000000..6bb8888 --- /dev/null +++ b/frontend/scripts/update-app.ps1 @@ -0,0 +1,12 @@ +# После правок в Cursor: собирает веб-приложение и копирует его в Android-проект. +# Запуск: powershell -ExecutionPolicy Bypass -File C:\Dev\Benchpress\frontend\scripts\update-app.ps1 + +$ErrorActionPreference = "Stop" +$frontend = Resolve-Path (Join-Path $PSScriptRoot "..") +Set-Location $frontend + +Write-Host "Сборка веб-части и cap sync..." +npm run cap:sync +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host "Готово. Дальше: .\build-apk.ps1, затем .\install-emulator.ps1" diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..2af5b1f --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,57 @@ +import { Navigate, Route, Routes } from "react-router-dom"; +import { AuthProvider, useAuth } from "./auth"; +import { HomePage } from "./pages/HomePage"; +import { LoginPage } from "./pages/LoginPage"; +import { OnboardingPage } from "./pages/OnboardingPage"; +import { SettingsPage } from "./pages/SettingsPage"; +import { WorkoutPage } from "./pages/WorkoutPage"; + +function Guard({ children }: { children: React.ReactNode }) { + const { loading, token, hasPassword, onboarded } = useAuth(); + if (loading) return

Загрузка…

; + if (!hasPassword || !token) return ; + if (!onboarded) return ; + return <>{children}; +} + +function AppRoutes() { + return ( + + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + ); +} + +export default function App() { + return ( + +
+ +
+
+ ); +} diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..655fe84 --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,172 @@ +import { localApi } from "./local/api"; +import { ApiError, type Profile, type WorkoutDetail, type WorkoutSummary } from "./apiTypes"; + +export { ApiError }; +export type { Exercise, Profile, WorkoutDetail, WorkoutSet, WorkoutSummary } from "./apiTypes"; + +const TOKEN_KEY = "bps_token"; +const OFFLINE = import.meta.env.VITE_OFFLINE === "true"; + +/** Пустой = относительные /api (web/nginx). Для online APK: полный URL. Offline — не используется. */ +const API_BASE = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.replace(/\/$/, "") || ""; + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + +export function setToken(token: string | null) { + if (token) localStorage.setItem(TOKEN_KEY, token); + else localStorage.removeItem(TOKEN_KEY); +} + +async function request(path: string, options: RequestInit = {}): Promise { + const headers = new Headers(options.headers || {}); + headers.set("Content-Type", "application/json"); + const token = getToken(); + if (token) headers.set("Authorization", `Bearer ${token}`); + + const url = path.startsWith("http") ? path : `${API_BASE}${path}`; + const res = await fetch(url, { ...options, headers }); + if (!res.ok) { + let msg = res.statusText; + try { + const data = await res.json(); + msg = typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data); + } catch { + /* ignore */ + } + throw new ApiError(res.status, msg); + } + if (res.status === 204) return undefined as T; + return res.json(); +} + +const remoteApi = { + status: () => request<{ has_password: boolean; onboarded: boolean; phase: string }>("/api/auth/status"), + setup: (password: string) => + request<{ access_token: string; onboarded: boolean }>("/api/auth/setup", { + method: "POST", + body: JSON.stringify({ password }), + }), + login: (password: string) => + request<{ access_token: string; onboarded: boolean }>("/api/auth/login", { + method: "POST", + body: JSON.stringify({ password }), + }), + profile: () => request("/api/profile"), + onboarding: (body: { one_rm_kg: number; training_weekdays: number[]; start_date: string }) => + request("/api/onboarding", { method: "POST", body: JSON.stringify(body) }), + calendar: (year: number, month: number) => + request<{ year: number; month: number; workouts: WorkoutSummary[]; profile: Profile }>( + `/api/calendar?year=${year}&month=${month}`, + ), + upcoming: () => request("/api/workouts/upcoming"), + workout: (id: number) => request(`/api/workouts/${id}`), + start: (id: number) => request(`/api/workouts/${id}/start`, { method: "POST" }), + pause: (id: number) => request(`/api/workouts/${id}/pause`, { method: "POST" }), + resume: (id: number) => request(`/api/workouts/${id}/resume`, { method: "POST" }), + completeSet: (id: number, body: { actual_reps?: number; actual_weight_kg?: number } = {}) => + request(`/api/workouts/${id}/complete-set`, { + method: "POST", + body: JSON.stringify(body), + }), + setWeight: (workoutId: number, exerciseId: number, weight_kg: number) => + request(`/api/workouts/${workoutId}/exercises/${exerciseId}/weight`, { + method: "POST", + body: JSON.stringify({ weight_kg }), + }), + adjustSet: ( + workoutId: number, + exerciseId: number, + setId: number, + body: { actual_weight_kg: number; actual_reps: number }, + ) => + request(`/api/workouts/${workoutId}/exercises/${exerciseId}/sets/${setId}`, { + method: "POST", + body: JSON.stringify(body), + }), + moveWorkout: (id: number, scheduledDate: string) => + request(`/api/workouts/${id}/move`, { + method: "POST", + body: JSON.stringify({ scheduled_date: scheduledDate }), + }), + updateTrainingDays: (training_weekdays: number[]) => + request("/api/training-days", { + method: "POST", + body: JSON.stringify({ training_weekdays }), + }), + prompt: ( + workoutId: number, + exerciseId: number, + body: { + choice: "easy_yes" | "easy_no"; + weight_kg?: number; + increase_kg?: number; + decrease_kg?: number; + }, + ) => + request(`/api/workouts/${workoutId}/exercises/${exerciseId}/prompt`, { + method: "POST", + body: JSON.stringify(body), + }), + startRest: (rest_days: number) => + request("/api/rest/start", { method: "POST", body: JSON.stringify({ rest_days }) }), + probe: (body: { one_rm_kg: number; start_date: string; training_weekdays?: number[] }) => + request("/api/probe", { method: "POST", body: JSON.stringify(body) }), + export: () => request<{ version: number; exported_at: string; data: unknown }>("/api/export"), + import: (payload: { version: number; data: unknown }) => + request<{ message: string }>("/api/import", { method: "POST", body: JSON.stringify(payload) }), + reset: () => request<{ message: string }>("/api/reset", { method: "POST" }), +}; + +export const api = OFFLINE ? localApi : remoteApi; +export const isOfflineMode = OFFLINE; + +export const WEEKDAY_LABELS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; +export const WEEKDAY_FULL = [ + "Понедельник", + "Вторник", + "Среда", + "Четверг", + "Пятница", + "Суббота", + "Воскресенье", +]; + +export function formatKg(v: number | null | undefined): string { + if (v == null) return "—"; + return Number.isInteger(v) ? `${v}` : `${v}`; +} + +export function formatTimer(totalSeconds: number): string { + const s = Math.max(0, Math.floor(totalSeconds)); + const m = Math.floor(s / 60); + const r = s % 60; + return `${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}`; +} + +export function formatRestCountdown(totalSeconds: number): string { + const s = Math.max(0, Math.floor(totalSeconds)); + const days = Math.floor(s / 86400); + const hours = Math.floor((s % 86400) / 3600); + const mins = Math.floor((s % 3600) / 60); + const secs = s % 60; + const n10 = days % 10; + const n100 = days % 100; + const word = n10 === 1 && n100 !== 11 ? "день" : n10 >= 2 && n10 <= 4 && (n100 < 12 || n100 > 14) ? "дня" : "дней"; + return `${String(days).padStart(2, "0")} ${word} ${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; +} + +export function formatDhms(totalSeconds: number): string { + const s = Math.max(0, Math.floor(totalSeconds)); + const days = Math.floor(s / 86400); + const hours = Math.floor((s % 86400) / 3600); + const mins = Math.floor((s % 3600) / 60); + const secs = s % 60; + return `${String(days).padStart(2, "0")}, ${String(hours).padStart(2, "0")}. ${String(mins).padStart(2, "0")}. ${String(secs).padStart(2, "0")}`; +} + +export function formatDateRu(isoDate: string): string { + const d = new Date(isoDate + "T12:00:00"); + return d.toLocaleDateString("ru-RU", { day: "numeric", month: "long" }); +} diff --git a/frontend/src/apiTypes.ts b/frontend/src/apiTypes.ts new file mode 100644 index 0000000..e780349 --- /dev/null +++ b/frontend/src/apiTypes.ts @@ -0,0 +1,75 @@ +export class ApiError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +export type Profile = { + one_rm_kg: number | null; + training_weekdays: number[] | null; + start_date: string | null; + onboarded: boolean; + phase: string; + rest_until: string | null; + probe_available_at: string | null; + cycle_number: number; + rest_countdown_seconds: number | null; + can_probe: boolean; +}; + +export type WorkoutSummary = { + id: number; + cycle_number: number; + week_number: number; + day_index: number; + scheduled_date: string; + status: string; + exercise_count: number; +}; + +export type WorkoutSet = { + id: number; + set_index: number; + target_percent: number | null; + target_reps: number | null; + target_weight_kg: number | null; + is_failure: boolean; + completed: boolean; + actual_reps: number | null; + actual_weight_kg: number | null; +}; + +export type Exercise = { + id: number; + order_index: number; + name: string; + kind: string; + prescribed_weight_kg: number | null; + weight_locked: boolean; + needs_weight_input: boolean; + pending_prompt: string | null; + weight_step_kg: number; + sets: WorkoutSet[]; +}; + +export type WorkoutDetail = { + id: number; + cycle_number: number; + week_number: number; + day_index: number; + scheduled_date: string; + status: string; + started_at: string | null; + paused_at: string | null; + completed_at: string | null; + current_exercise_index: number; + current_set_index: number; + rest_started_at: string | null; + rest_elapsed_seconds: number; + can_start: boolean; + can_resume: boolean; + pause_expires_at: string | null; + exercises: Exercise[]; +}; diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/auth.tsx b/frontend/src/auth.tsx new file mode 100644 index 0000000..961f65e --- /dev/null +++ b/frontend/src/auth.tsx @@ -0,0 +1,80 @@ +import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; +import { api, getToken, setToken } from "./api"; + +type AuthState = { + loading: boolean; + hasPassword: boolean; + onboarded: boolean; + token: string | null; + refresh: () => Promise; + login: (password: string) => Promise; + setup: (password: string) => Promise; + logout: () => void; +}; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [loading, setLoading] = useState(true); + const [hasPassword, setHasPassword] = useState(false); + const [onboarded, setOnboarded] = useState(false); + const [token, setTokenState] = useState(getToken()); + + const refresh = async () => { + const status = await api.status(); + setHasPassword(status.has_password); + setOnboarded(status.onboarded); + if (getToken()) { + try { + const profile = await api.profile(); + setOnboarded(profile.onboarded); + } catch { + setToken(null); + setTokenState(null); + } + } + }; + + useEffect(() => { + refresh() + .catch(() => undefined) + .finally(() => setLoading(false)); + }, []); + + const value = useMemo( + () => ({ + loading, + hasPassword, + onboarded, + token, + refresh, + async login(password: string) { + const res = await api.login(password); + setToken(res.access_token); + setTokenState(res.access_token); + setOnboarded(res.onboarded); + setHasPassword(true); + }, + async setup(password: string) { + const res = await api.setup(password); + setToken(res.access_token); + setTokenState(res.access_token); + setOnboarded(res.onboarded); + setHasPassword(true); + }, + logout() { + setToken(null); + setTokenState(null); + }, + }), + [loading, hasPassword, onboarded, token], + ); + + return {children}; +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("AuthProvider missing"); + return ctx; +} diff --git a/frontend/src/components/AppCalendar.tsx b/frontend/src/components/AppCalendar.tsx new file mode 100644 index 0000000..b06d419 --- /dev/null +++ b/frontend/src/components/AppCalendar.tsx @@ -0,0 +1,80 @@ +import { useMemo, useState } from "react"; +import { WEEKDAY_LABELS, type WorkoutSummary } from "../api"; + +function daysInMonth(year: number, month: number) { + return new Date(year, month, 0).getDate(); +} + +export function AppCalendar({ + value, + workouts = [], + onPick, +}: { + value: string; + workouts?: WorkoutSummary[]; + onPick: (iso: string) => void; +}) { + const initial = new Date(value + "T12:00:00"); + const [year, setYear] = useState(initial.getFullYear()); + const [month, setMonth] = useState(initial.getMonth() + 1); + const byDate = useMemo(() => { + const map = new Map(); + for (const w of workouts) map.set(w.scheduled_date, w); + return map; + }, [workouts]); + const firstWeekday = (new Date(year, month - 1, 1).getDay() + 6) % 7; + const totalDays = daysInMonth(year, month); + const today = new Date(); + const todayIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; + const shift = (delta: number) => { + const d = new Date(year, month - 1 + delta, 1); + setYear(d.getFullYear()); + setMonth(d.getMonth() + 1); + }; + const statusClass = (s: string) => { + if (s === "completed") return "done"; + if (s === "in_progress" || s === "paused") return "active"; + if (s === "abandoned") return "missed"; + return "planned"; + }; + + return ( +
+
+ +

{new Date(year, month - 1, 1).toLocaleString("ru-RU", { month: "long", year: "numeric" })}

+ +
+
+ {WEEKDAY_LABELS.map((d) => ( +
{d}
+ ))} +
+
+ {Array.from({ length: firstWeekday }).map((_, i) => ( +
+ ))} + {Array.from({ length: totalDays }).map((_, i) => { + const day = i + 1; + const key = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const w = byDate.get(key); + return ( + + ); + })} +
+
+ ); +} diff --git a/frontend/src/components/WeightStepper.tsx b/frontend/src/components/WeightStepper.tsx new file mode 100644 index 0000000..288521e --- /dev/null +++ b/frontend/src/components/WeightStepper.tsx @@ -0,0 +1,45 @@ +import type { ReactNode } from "react"; + +type WeightStepperProps = { + value: number; + step?: number; + min?: number; + onChange: (value: number) => void; + label?: string; + compact?: boolean; + action?: ReactNode; +}; + +export function WeightStepper({ + value, + step = 0.5, + min = 0, + onChange, + label, + compact, + action, +}: WeightStepperProps) { + const bump = (delta: number) => { + const next = Math.round((value + delta) / step) * step; + onChange(Math.max(min, Math.round(next * 100) / 100)); + }; + + return ( +
+ {label && {label}} +
+ +
+ {Number.isInteger(value) ? value : value.toFixed(1)} + кг +
+ + {action} +
+
+ ); +} diff --git a/frontend/src/data/program.json b/frontend/src/data/program.json new file mode 100644 index 0000000..1f1ce1d --- /dev/null +++ b/frontend/src/data/program.json @@ -0,0 +1,1689 @@ +{ + "weeks": [ + { + "week": 1, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 60.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 4 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 6 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 55.0, + "reps": 6 + }, + { + "percent": 45.0, + "reps": 8 + } + ] + }, + { + "name": "Молотковые сгибания", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + } + ] + }, + { + "name": "Разгибания с канатом", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + } + ] + }, + { + "week": 2, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + } + ] + }, + { + "name": "Вертикальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Обратная планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 20 + }, + { + "type": "reps", + "reps": 20 + }, + { + "type": "reps", + "reps": 20 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 75.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 6 + }, + { + "percent": 45.0, + "reps": 8 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + } + ] + }, + { + "week": 3, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 4 + }, + { + "percent": 80.0, + "reps": 4 + }, + { + "percent": 80.0, + "reps": 4 + } + ] + }, + { + "name": "Молотковые сгибания", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Разгибания с канатом", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + } + ] + }, + { + "name": "Вертикальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Горизонтальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Обратная планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 85.0, + "reps": 4 + }, + { + "percent": 85.0, + "reps": 4 + }, + { + "percent": 85.0, + "reps": 4 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 5 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + } + ] + }, + { + "week": 4, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 2 + }, + { + "percent": 80.0, + "reps": 5 + }, + { + "percent": 80.0, + "reps": 5 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + }, + { + "type": "reps", + "reps": 15 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 5 + } + ] + }, + { + "name": "Молотковые сгибания", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + }, + { + "type": "reps", + "reps": 8 + } + ] + }, + { + "name": "Разгибания с канатом", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 6 + }, + { + "percent": 80.0, + "reps": 6 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 6 + }, + { + "percent": 45.0, + "reps": 8 + } + ] + }, + { + "name": "Вертикальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальная тяга", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Обратная планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + } + ] + }, + { + "week": 5, + "days": [ + { + "template_day": "monday", + "label": "Понедельник", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 3 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 5 + }, + { + "percent": 80.0, + "reps": 5 + } + ] + }, + { + "name": "Строгий на бицепс", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + }, + { + "type": "reps", + "reps": 6 + } + ] + }, + { + "name": "Французский жим гантелей", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Планка", + "kind": "failure", + "sets": [ + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + }, + { + "type": "failure", + "sets": 1 + } + ] + } + ] + }, + { + "template_day": "wednesday", + "label": "Среда", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 6 + }, + { + "percent": 55.0, + "reps": 5 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 65.0, + "reps": 2 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + }, + { + "percent": 70.0, + "reps": 5 + } + ] + }, + { + "name": "Пуловер", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + }, + { + "type": "reps", + "reps": 12 + } + ] + }, + { + "name": "Горизонтальные отведения", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + }, + { + "name": "Гиперэкстензия", + "kind": "accessory", + "sets": [ + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + }, + { + "type": "reps", + "reps": 10 + } + ] + } + ] + }, + { + "template_day": "friday", + "label": "Пятница", + "exercises": [ + { + "name": "Жим", + "kind": "bench", + "sets": [ + { + "percent": 45.0, + "reps": 5 + }, + { + "percent": 55.0, + "reps": 4 + }, + { + "percent": 65.0, + "reps": 3 + }, + { + "percent": 70.0, + "reps": 2 + }, + { + "percent": 75.0, + "reps": 3 + }, + { + "percent": 80.0, + "reps": 3 + }, + { + "percent": 85.0, + "reps": 2 + }, + { + "percent": 90.0, + "reps": 4 + }, + { + "percent": 90.0, + "reps": 4 + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..5c024bf --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,844 @@ +:root { + --bg0: #0f1412; + --bg1: #18201c; + --bg2: #223028; + --line: #2f3f36; + --text: #eef3ef; + --muted: #9aada2; + --accent: #c4f04d; + --accent-dim: #8fb82f; + --danger: #ff6b5a; + --ok: #7dffb3; + --radius: 14px; + --font: "Segoe UI", "Manrope", system-ui, sans-serif; + color: var(--text); + background: + radial-gradient(1200px 600px at 10% -10%, #1d3324 0%, transparent 55%), + radial-gradient(900px 500px at 100% 0%, #24361f 0%, transparent 50%), + linear-gradient(180deg, #101612, #0c100e); + font-family: var(--font); + line-height: 1.4; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + height: 100%; + overflow: hidden; +} + +body { + min-width: 320px; + overflow: hidden; +} + +button, +input { + font: inherit; +} + +a { + color: inherit; +} + +.app-shell { + height: 100dvh; + max-height: 100dvh; + max-width: 480px; + margin: 0 auto; + padding-top: max(28px, env(safe-area-inset-top, 0px)); + overflow: hidden; + display: flex; + flex-direction: column; + background: #0f1412; +} + +.page { + flex: 1; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 16px 16px calc(24px + env(safe-area-inset-bottom, 0px)); + display: flex; + flex-direction: column; + gap: 16px; +} + +.settings-page { + min-height: 100%; +} + +.settings-foot { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 12px; +} + +.brand { + margin: 0; + font-size: 1.35rem; + font-weight: 750; + letter-spacing: -0.02em; +} + +.brand-block { + padding: 12px 4px 4px; +} + +.muted { + color: var(--muted); + margin: 4px 0 0; +} + +.hint { + display: block; + color: var(--muted); + font-size: 0.85rem; + margin-top: 6px; +} + +.error { + color: var(--danger); + margin: 0; +} + +.ok { + color: var(--ok); +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.topbar-actions { + margin-left: auto; + display: flex; + gap: 8px; +} + +.card-form, +.phase-banner, +.active-set, +.prompt-box, +.settings-list { + background: color-mix(in srgb, var(--bg1) 88%, black); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 16px; +} + +.card-form, +.settings-list, +.prompt-box, +.active-set, +.phase-banner { + display: flex; + flex-direction: column; + gap: 12px; +} + +label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.92rem; +} + +input[type="number"], +input[type="password"], +input[type="date"], +input[type="text"] { + width: 100%; + border: 1px solid var(--line); + background: var(--bg0); + color: var(--text); + border-radius: 10px; + padding: 12px 12px; +} + +fieldset { + border: 0; + padding: 0; + margin: 0; +} + +legend { + margin-bottom: 8px; +} + +.btn { + border: 1px solid var(--line); + background: var(--bg2); + color: var(--text); + border-radius: 12px; + padding: 12px 14px; + cursor: pointer; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn.primary { + background: linear-gradient(180deg, var(--accent), var(--accent-dim)); + color: #10140f; + border-color: transparent; + font-weight: 700; +} + +.btn.ghost { + background: transparent; +} + +.btn.danger { + background: color-mix(in srgb, var(--danger) 20%, var(--bg2)); + border-color: color-mix(in srgb, var(--danger) 50%, var(--line)); + color: #ffd2cc; +} + +.btn.wide { + width: 100%; + padding: 16px; + font-size: 1.05rem; +} + +.day-grid, +.row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.chip { + border: 1px solid var(--line); + background: var(--bg0); + color: var(--text); + border-radius: 999px; + padding: 10px 12px; + cursor: pointer; +} + +.chip.on { + background: color-mix(in srgb, var(--accent) 25%, var(--bg0)); + border-color: var(--accent-dim); +} + +.week-strip h2, +.calendar h2, +.exercise-list h2 { + margin: 0 0 10px; + font-size: 1rem; +} + +.strip-row { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(92px, 1fr); + gap: 8px; + overflow-x: auto; + padding-bottom: 4px; +} + +.strip-card { + border: 1px solid var(--line); + background: var(--bg1); + color: var(--text); + border-radius: 12px; + padding: 12px 10px; + display: flex; + flex-direction: column; + gap: 4px; + text-align: left; + cursor: pointer; +} + +.strip-card.today { + outline: 2px solid var(--accent); +} + +.strip-card.done { + border-color: color-mix(in srgb, var(--ok) 40%, var(--line)); +} + +.strip-card.active { + border-color: var(--accent-dim); +} + +.strip-day { + font-size: 0.8rem; + color: var(--muted); +} + +.strip-date { + font-size: 1.3rem; + font-weight: 700; +} + +.strip-meta { + font-size: 0.75rem; + color: var(--muted); +} + +.cal-nav { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.cal-nav h2 { + margin: 0; + text-transform: capitalize; + font-size: 1.05rem; +} + +.cal-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 6px; +} + +.cal-grid.head { + margin-bottom: 4px; + color: var(--muted); + font-size: 0.78rem; + text-align: center; +} + +.cal-cell { + aspect-ratio: 1; + border: 1px solid transparent; + background: transparent; + color: var(--text); + border-radius: 10px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: 0; +} + +.cal-cell:disabled { + opacity: 0.55; +} + +.cal-cell.planned, +.cal-cell.done, +.cal-cell.active, +.cal-cell.missed { + background: var(--bg1); + border-color: var(--line); + cursor: pointer; +} + +.cal-cell.today { + box-shadow: inset 0 0 0 1px var(--accent); +} + +.cal-cell .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); +} + +.cal-cell.done .dot { + background: var(--ok); +} + +.cal-cell.picked { + box-shadow: inset 0 0 0 2px var(--accent); +} + +.cal-grid:not(.head) { + touch-action: none; +} + +.cal-cell.drop { + box-shadow: inset 0 0 0 2px #3d7ea6; +} + +.cal-cell.dragging { + opacity: 0.45; +} + +.drag-ghost { + position: fixed; + z-index: 30; + width: 44px; + height: 44px; + margin-left: -22px; + margin-top: -46px; + border-radius: 10px; + display: grid; + place-items: center; + pointer-events: none; + background: var(--bg2); + border: 2px solid #3d7ea6; + font-weight: 700; +} + +.workout-page { + min-height: 100%; +} + +.workout-foot { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 12px; +} + +.btn.move { + background: #3d7ea6; + border-color: transparent; + color: #f4fbff; + font-weight: 700; +} + +.rest-until { + display: flex; + flex-direction: column; + gap: 2px; +} + +.rest-title { + margin: 0; + font-size: 1.15rem; + font-weight: 750; +} + +.rest-until p { + margin: 0; +} + +.move-prompt { + margin: 0; + font-size: 1.05rem; + font-weight: 700; +} + +.move-calendar { + margin-top: 4px; +} + +.status-line { + font-variant-numeric: tabular-nums; +} + +.active-set .ex-name { + margin: 0; + font-size: 1.2rem; + font-weight: 700; +} + +.set-label { + margin: 0; + color: var(--muted); +} + +.big-numbers { + display: flex; + justify-content: space-around; + gap: 12px; + padding: 10px 0 4px; +} + +.big-numbers .num { + font-size: 2.4rem; + font-weight: 800; + letter-spacing: -0.03em; +} + +.big-numbers .unit { + display: block; + color: var(--muted); + font-size: 0.85rem; + text-align: center; +} + +.exercise-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.ex-block { + background: color-mix(in srgb, var(--bg1) 88%, black); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 0; + overflow: hidden; +} + +.ex-block.current { + border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); +} + +.ex-block.finished:not(.current) { + opacity: 0.78; +} + +.ex-head { + display: flex; + align-items: center; +} + +.ex-toggle { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + border: 0; + background: transparent; + color: var(--text); + padding: 14px 8px 14px 16px; + cursor: pointer; + text-align: left; +} + +.icon-btn { + width: 44px; + height: 44px; + padding: 0; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 12px; + background: transparent; + color: var(--text); + cursor: pointer; +} + +.ex-edit { + flex-shrink: 0; + width: 36px; + height: 36px; + margin-right: 10px; + padding: 0; + display: grid; + place-items: center; + border: 1px solid var(--line); + border-radius: 10px; + background: transparent; + color: var(--muted); + cursor: pointer; +} + +.ex-edit.on { + color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); +} + +.ex-toggle-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.ex-toggle-main strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ex-toggle-meta { + flex-shrink: 0; + font-size: 0.85rem; +} + +.chevron { + color: var(--muted); + width: 1rem; +} + +.badge-now, +.badge-done { + font-size: 0.72rem; + font-weight: 700; + border-radius: 999px; + padding: 2px 8px; + flex-shrink: 0; +} + +.badge-now { + background: color-mix(in srgb, var(--accent) 22%, transparent); + color: var(--accent); +} + +.badge-done { + background: color-mix(in srgb, var(--ok) 18%, transparent); + color: var(--ok); +} + +.ex-body { + padding: 14px 16px 14px; + border-top: 1px solid var(--line); +} + +.ex-body .active-set.nested, +.ex-body .prompt-box.nested { + margin: 0 0 12px; + border-radius: 12px; +} + +.ex-body ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.ex-body li { + color: var(--muted); + font-size: 1.05rem; + line-height: 1.4; + min-height: 40px; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.set-adjust { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.set-step { + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--bg2); + color: var(--text); + font-size: 1.05rem; + line-height: 1; + cursor: pointer; +} + +.set-step:disabled { + opacity: 0.45; +} + +.set-step-label { + min-width: 2.4rem; + text-align: center; + font-size: 0.75rem; + color: var(--muted); +} + +.ex-body li.done { + color: var(--ok); +} + +.weight-stepper { + display: flex; + flex-direction: column; + gap: 8px; +} + +.weight-stepper-label { + font-size: 0.92rem; +} + +.weight-stepper-row { + display: grid; + grid-template-columns: 44px 1fr 44px; + gap: 8px; + align-items: center; +} + +.weight-stepper .rm-btn { + height: 40px; + font-size: 1.25rem; +} + +.weight-stepper-value { + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + padding: 2px 8px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--bg0); +} + +.weight-stepper-value .num { + font-size: 1.25rem; + font-weight: 800; + line-height: 1.1; +} + +.weight-stepper-value .unit { + font-size: 0.72rem; +} + +.weight-stepper.compact .weight-stepper-row { + display: flex; + justify-content: flex-start; +} + +.weight-stepper.compact .weight-stepper-value { + width: 76px; + flex: 0 0 76px; + padding: 0 4px; +} + +.weight-stepper.compact .rm-btn { + width: 40px; + flex: 0 0 40px; +} + +.check-btn { + width: 40px; + height: 40px; + flex: 0 0 40px; + border-radius: 10px; + background: var(--accent); + border-color: transparent; + color: #10140f; +} + +.check-btn:disabled { + opacity: 0.45; +} + +.auth-page { + min-height: 100%; + justify-content: center; +} + +.stepper { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} + +.step-dot { + border: 1px solid var(--line); + background: var(--bg1); + color: var(--muted); + border-radius: 999px; + padding: 8px 10px; + font-size: 0.85rem; + cursor: pointer; +} + +.step-dot.on { + color: #10140f; + background: var(--accent); + border-color: transparent; + font-weight: 700; +} + +.step-dot.done { + border-color: var(--accent-dim); + color: var(--text); +} + +.onboard-step h1 { + margin: 0; + font-size: 1.35rem; +} + +.rm-picker { + display: grid; + grid-template-columns: 64px 1fr 64px; + gap: 10px; + align-items: center; + margin: 8px 0 4px; +} + +.rm-btn { + height: 64px; + font-size: 1.8rem; + font-weight: 700; + padding: 0; +} + +.rm-value { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.rm-value input { + text-align: center; + font-size: 2rem; + font-weight: 800; + padding: 14px 10px; +} + +.rm-value span { + color: var(--muted); + font-size: 0.9rem; +} + +.day-grid-lg { + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.chip-lg { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 12px; + padding: 14px 16px; + font-size: 1.05rem; + text-align: left; +} + +.chip-check { + color: var(--accent); + font-weight: 700; +} + +.summary-box { + background: var(--bg0); + border: 1px solid var(--line); + border-radius: 12px; + padding: 12px 14px; +} + +.summary-box p { + margin: 0 0 6px; +} + +.summary-box p:last-child { + margin: 0; +} + +@media (min-width: 480px) { + .app-shell { + border-left: 1px solid var(--line); + border-right: 1px solid var(--line); + } +} diff --git a/frontend/src/local/api.ts b/frontend/src/local/api.ts new file mode 100644 index 0000000..96e54c6 --- /dev/null +++ b/frontend/src/local/api.ts @@ -0,0 +1,711 @@ +import { ApiError, type Exercise, type Profile, type WorkoutDetail, type WorkoutSummary } from "../apiTypes"; +import { db, ensureProfile } from "./db"; +import { planMove, planWeekdayChange } from "./reschedule"; +import { + applyAccessoryWeightToFuture, + generateSchedule, + todayIso, + weightStepFor, +} from "./schedule"; +import type { StoredExercise, StoredWorkout } from "./types"; +import { PAUSE_LIMIT_MINUTES, roundExerciseWeight, roundToStep, ACCESSORY_STEP_KG } from "./weights"; + +async function hashPassword(password: string): Promise { + const data = new TextEncoder().encode(`bps:${password}`); + const buf = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(buf)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +function isoNow(): string { + return new Date().toISOString(); +} + +function addDaysIso(isoDate: string, days: number): string { + const d = new Date(isoDate + "T12:00:00"); + d.setDate(d.getDate() + days); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + +function profileView(p: Awaited>): Profile { + const now = Date.now(); + let restCountdown: number | null = null; + let canProbe = false; + let phase = p.phase; + if (p.phase === "rest" && p.probe_available_at) { + const left = Math.floor((new Date(p.probe_available_at).getTime() - now) / 1000); + if (left > 0) restCountdown = left; + else { + canProbe = true; + phase = "probe"; + } + } else if (p.phase === "probe") { + canProbe = true; + } + return { + one_rm_kg: p.one_rm_kg, + training_weekdays: p.training_weekdays, + start_date: p.start_date, + onboarded: p.onboarded, + phase, + rest_until: p.rest_until, + probe_available_at: p.probe_available_at, + cycle_number: p.cycle_number, + rest_countdown_seconds: restCountdown, + can_probe: canProbe, + }; +} + +function summaryOf(w: StoredWorkout): WorkoutSummary { + return { + id: w.id, + cycle_number: w.cycle_number, + week_number: w.week_number, + day_index: w.day_index, + scheduled_date: w.scheduled_date, + status: w.status, + exercise_count: w.exercises.length, + }; +} + +function expirePause(w: StoredWorkout): StoredWorkout { + if (w.status === "paused" && w.paused_at) { + const limit = new Date(w.paused_at).getTime() + PAUSE_LIMIT_MINUTES * 60_000; + if (Date.now() > limit) { + w.status = "abandoned"; + w.paused_at = null; + w.rest_started_at = null; + } + } + return w; +} + +function toExercise(ex: StoredExercise): Exercise { + return { + id: ex.id, + order_index: ex.order_index, + name: ex.name, + kind: ex.kind, + prescribed_weight_kg: ex.prescribed_weight_kg, + weight_locked: ex.weight_locked, + needs_weight_input: ex.needs_weight_input, + pending_prompt: ex.pending_prompt, + weight_step_kg: weightStepFor(ex.name, ex.kind), + sets: ex.sets + .slice() + .sort((a, b) => a.set_index - b.set_index) + .map((s) => ({ + id: s.id, + set_index: s.set_index, + target_percent: s.target_percent, + target_reps: s.target_reps, + target_weight_kg: s.target_weight_kg, + is_failure: s.is_failure, + completed: s.completed, + actual_reps: s.actual_reps, + actual_weight_kg: s.actual_weight_kg, + })), + }; +} + +async function syncAccessories(w: StoredWorkout): Promise { + let changed = false; + for (const ex of w.exercises) { + if (ex.kind !== "accessory") continue; + if (ex.sets.length > 0 && ex.sets.every((s) => s.completed)) continue; + const st = await db.accessories + .where({ cycle_number: w.cycle_number, exercise_name: ex.name }) + .first(); + if (!st) continue; + const weight = st.pending_weight_kg ?? st.weight_kg; + if (weight == null) continue; + if ( + ex.prescribed_weight_kg !== weight || + ex.needs_weight_input || + ex.weight_locked !== st.locked + ) { + ex.prescribed_weight_kg = weight; + ex.needs_weight_input = false; + ex.weight_locked = st.locked; + for (const s of ex.sets) { + if (!s.completed) s.target_weight_kg = weight; + } + changed = true; + } + } + if (changed) await db.workouts.put(w); + return w; +} + +async function getWorkout(id: number): Promise { + let w = await db.workouts.get(id); + if (!w) throw new ApiError(404, "Тренировка не найдена"); + w = expirePause(w); + await db.workouts.put(w); + w = await syncAccessories(w); + return w; +} + +function detailOf(w: StoredWorkout, all: StoredWorkout[]): WorkoutDetail { + const today = todayIso(); + const now = Date.now(); + const active = all.find( + (x) => x.id !== w.id && x.cycle_number === w.cycle_number && (x.status === "in_progress" || x.status === "paused"), + ); + const canStart = w.status === "planned" && w.scheduled_date === today && !active; + let canResume = false; + let pauseExpires: string | null = null; + if (w.status === "paused" && w.paused_at) { + const exp = new Date(w.paused_at).getTime() + PAUSE_LIMIT_MINUTES * 60_000; + pauseExpires = new Date(exp).toISOString(); + canResume = now <= exp && w.scheduled_date === today; + } + let restElapsed = 0; + if (w.status === "in_progress" && w.rest_started_at) { + restElapsed = Math.max(0, Math.floor((now - new Date(w.rest_started_at).getTime()) / 1000)); + } + return { + id: w.id, + cycle_number: w.cycle_number, + week_number: w.week_number, + day_index: w.day_index, + scheduled_date: w.scheduled_date, + status: w.status, + started_at: w.started_at, + paused_at: w.paused_at, + completed_at: w.completed_at, + current_exercise_index: w.current_exercise_index, + current_set_index: w.current_set_index, + rest_started_at: w.rest_started_at, + rest_elapsed_seconds: restElapsed, + can_start: canStart, + can_resume: canResume, + pause_expires_at: pauseExpires, + exercises: w.exercises.slice().sort((a, b) => a.order_index - b.order_index).map(toExercise), + }; +} + +function advancePointer(w: StoredWorkout): boolean { + const exercises = w.exercises.slice().sort((a, b) => a.order_index - b.order_index); + for (let ei = 0; ei < exercises.length; ei++) { + const sets = exercises[ei].sets.slice().sort((a, b) => a.set_index - b.set_index); + for (const s of sets) { + if (!s.completed) { + w.current_exercise_index = ei; + w.current_set_index = s.set_index; + return false; + } + } + } + return true; +} + +async function maybeFinishCycle(w: StoredWorkout): Promise { + const others = await db.workouts + .where("cycle_number") + .equals(w.cycle_number) + .filter((x) => x.id !== w.id && !["completed", "abandoned"].includes(x.status)) + .count(); + if (others === 0) { + const p = await ensureProfile(); + p.phase = "rest"; + p.probe_available_at = null; + p.rest_until = null; + await db.profile.put(p); + } +} + +export const localApi = { + async status() { + const auth = await db.auth.get(1); + const p = await ensureProfile(); + return { has_password: !!auth, onboarded: p.onboarded, phase: p.phase }; + }, + + async setup(password: string) { + if (await db.auth.get(1)) throw new ApiError(400, "Пароль уже задан"); + await db.auth.put({ id: 1, password_hash: await hashPassword(password) }); + await ensureProfile(); + const p = await ensureProfile(); + return { access_token: "local", onboarded: p.onboarded }; + }, + + async login(password: string) { + const auth = await db.auth.get(1); + if (!auth) throw new ApiError(400, "Сначала задайте пароль"); + if (auth.password_hash !== (await hashPassword(password))) throw new ApiError(401, "Неверный пароль"); + const p = await ensureProfile(); + return { access_token: "local", onboarded: p.onboarded }; + }, + + async profile() { + return profileView(await ensureProfile()); + }, + + async onboarding(body: { one_rm_kg: number; training_weekdays: number[]; start_date: string }) { + const p = await ensureProfile(); + if (p.onboarded) throw new ApiError(400, "Онбординг уже пройден. Используйте сброс в настройках."); + const oneRm = roundToStep(body.one_rm_kg, 1.25); + p.one_rm_kg = oneRm; + p.training_weekdays = [...body.training_weekdays].sort((a, b) => a - b); + p.start_date = body.start_date; + p.onboarded = true; + p.phase = "active"; + p.cycle_number = 1; + await db.profile.put(p); + await db.workouts.where("cycle_number").equals(1).delete(); + await db.accessories.where("cycle_number").equals(1).delete(); + await generateSchedule({ + oneRmKg: oneRm, + trainingWeekdays: p.training_weekdays, + startDate: body.start_date, + cycleNumber: 1, + }); + return profileView(p); + }, + + async calendar(year: number, month: number) { + const p = await ensureProfile(); + const workouts = await db.workouts.where("cycle_number").equals(p.cycle_number).toArray(); + const monthWorkouts = workouts + .filter((w) => { + const [y, m] = w.scheduled_date.split("-").map(Number); + return y === year && m === month; + }) + .sort((a, b) => a.scheduled_date.localeCompare(b.scheduled_date)); + return { + year, + month, + workouts: monthWorkouts.map(summaryOf), + profile: profileView(p), + }; + }, + + async upcoming() { + const p = await ensureProfile(); + const workouts = await db.workouts + .where("cycle_number") + .equals(p.cycle_number) + .sortBy("scheduled_date"); + return workouts.map(summaryOf); + }, + + async workout(id: number) { + const w = await getWorkout(id); + const all = await db.workouts.where("cycle_number").equals(w.cycle_number).toArray(); + return detailOf(w, all); + }, + + async start(id: number) { + const w = await getWorkout(id); + const today = todayIso(); + if (w.scheduled_date !== today) throw new ApiError(400, "Начать можно только сегодняшнюю тренировку"); + if (w.status !== "planned") throw new ApiError(400, "Тренировку нельзя начать"); + const active = await db.workouts + .where("cycle_number") + .equals(w.cycle_number) + .filter((x) => x.status === "in_progress" || x.status === "paused") + .first(); + if (active) throw new ApiError(400, "Уже есть активная тренировка"); + w.status = "in_progress"; + w.started_at = isoNow(); + w.current_exercise_index = 0; + w.current_set_index = 0; + await db.workouts.put(w); + return this.workout(id); + }, + + async pause(id: number) { + const w = await getWorkout(id); + if (w.status !== "in_progress") throw new ApiError(400, "Пауза доступна только во время тренировки"); + w.status = "paused"; + w.paused_at = isoNow(); + w.rest_started_at = null; + await db.workouts.put(w); + return this.workout(id); + }, + + async resume(id: number) { + let w = await getWorkout(id); + if (w.status === "abandoned") throw new ApiError(400, "Тренировка остановлена: пауза дольше 15 минут"); + if (w.status !== "paused") throw new ApiError(400, "Тренировка не на паузе"); + if (w.scheduled_date !== todayIso()) throw new ApiError(400, "Продолжить можно только в тот же день"); + if (!w.paused_at || Date.now() > new Date(w.paused_at).getTime() + PAUSE_LIMIT_MINUTES * 60_000) { + w.status = "abandoned"; + await db.workouts.put(w); + throw new ApiError(400, "Тренировка остановлена: пауза дольше 15 минут"); + } + w.status = "in_progress"; + w.paused_at = null; + await db.workouts.put(w); + return this.workout(id); + }, + + async setWeight(workoutId: number, exerciseId: number, weightKg: number) { + const w = await getWorkout(workoutId); + if (w.status === "completed") throw new ApiError(400, "Выполненную тренировку менять нельзя"); + const ex = w.exercises.find((e) => e.id === exerciseId); + if (!ex) throw new ApiError(404, "Упражнение не найдено"); + if (ex.kind !== "accessory") throw new ApiError(400, "Вес задаётся только для вспомогательных"); + if (ex.sets.length > 0 && ex.sets.every((s) => s.completed)) { + throw new ApiError(400, "Это упражнение уже выполнено"); + } + const weight = roundExerciseWeight(weightKg, ex.name, ex.kind); + ex.prescribed_weight_kg = weight; + ex.needs_weight_input = false; + for (const s of ex.sets) { + if (!s.completed) s.target_weight_kg = weight; + } + await db.workouts.put(w); + await applyAccessoryWeightToFuture({ + cycleNumber: w.cycle_number, + exerciseName: ex.name, + weightKg: weight, + fromDate: w.scheduled_date, + lock: false, + }); + const fresh = await db.workouts.get(workoutId); + if (fresh) { + const current = fresh.exercises.find((e) => e.id === exerciseId); + if (current) { + current.prescribed_weight_kg = weight; + current.needs_weight_input = false; + for (const s of current.sets) { + if (!s.completed) s.target_weight_kg = weight; + } + await db.workouts.put(fresh); + } + } + return this.workout(workoutId); + }, + + async adjustSet( + workoutId: number, + exerciseId: number, + setId: number, + body: { actual_weight_kg: number; actual_reps: number }, + ) { + const w = await getWorkout(workoutId); + const ex = w.exercises.find((e) => e.id === exerciseId); + if (!ex) throw new ApiError(404, "Упражнение не найдено"); + if (ex.kind !== "accessory") throw new ApiError(400, "Править можно только вспомогательные"); + const set = ex.sets.find((s) => s.id === setId); + if (!set || !set.completed) throw new ApiError(400, "Можно править только выполненный подход"); + set.actual_weight_kg = roundExerciseWeight(body.actual_weight_kg, ex.name, ex.kind); + set.actual_reps = Math.max(0, Math.round(body.actual_reps)); + await db.workouts.put(w); + return this.workout(workoutId); + }, + + async moveWorkout(id: number, scheduledDate: string) { + const w = await getWorkout(id); + const p = await ensureProfile(); + const weekdays = p.training_weekdays; + if (!weekdays || weekdays.length === 0) throw new ApiError(400, "Не заданы дни тренировок"); + const all = await db.workouts.where("cycle_number").equals(w.cycle_number).toArray(); + let assigned: Map; + try { + assigned = planMove(all, id, scheduledDate, weekdays); + } catch (err) { + throw new ApiError(400, err instanceof Error ? err.message : "Не удалось перенести"); + } + for (const item of all) { + const next = assigned.get(item.id); + if (next && next !== item.scheduled_date) { + item.scheduled_date = next; + await db.workouts.put(item); + } + } + return this.workout(id); + }, + + async updateTrainingDays(weekdays: number[]) { + const p = await ensureProfile(); + if (!p.onboarded) throw new ApiError(400, "Сначала создайте программу"); + const all = await db.workouts.where("cycle_number").equals(p.cycle_number).toArray(); + const done = all + .filter((item) => item.status === "completed") + .map((item) => item.scheduled_date) + .sort(); + let start = todayIso(); + if (done.length) { + const after = addDaysIso(done[done.length - 1], 1); + if (after > start) start = after; + } + let assigned: Map; + try { + assigned = planWeekdayChange(all, weekdays, start); + } catch (err) { + throw new ApiError(400, err instanceof Error ? err.message : "Не удалось обновить дни"); + } + for (const item of all) { + const next = assigned.get(item.id); + if (next && next !== item.scheduled_date) { + item.scheduled_date = next; + await db.workouts.put(item); + } + } + p.training_weekdays = [...weekdays].sort((a, b) => a - b); + await db.profile.put(p); + return profileView(p); + }, + + async completeSet(id: number, body: { actual_reps?: number; actual_weight_kg?: number } = {}) { + const w = await getWorkout(id); + if (w.status !== "in_progress") throw new ApiError(400, "Тренировка не активна"); + const exercises = w.exercises.slice().sort((a, b) => a.order_index - b.order_index); + const exercise = exercises[w.current_exercise_index]; + if (!exercise) throw new ApiError(400, "Нет текущего упражнения"); + if (exercise.pending_prompt) throw new ApiError(400, "Сначала ответьте на вопрос по упражнению"); + if (exercise.kind === "accessory" && exercise.needs_weight_input) { + throw new ApiError(400, "Сначала введите рабочий вес"); + } + let current = + exercise.sets.find((s) => s.set_index === w.current_set_index && !s.completed) || + exercise.sets.slice().sort((a, b) => a.set_index - b.set_index).find((s) => !s.completed); + if (!current) throw new ApiError(400, "Нет активного подхода"); + + if (exercise.kind === "bench" || exercise.kind === "failure") { + current.completed = true; + current.completed_at = isoNow(); + current.actual_weight_kg = current.target_weight_kg; + current.actual_reps = current.target_reps; + } else { + if (body.actual_reps == null) throw new ApiError(400, "Укажите выполненные повторения"); + const weight = body.actual_weight_kg ?? exercise.prescribed_weight_kg; + if (weight == null) throw new ApiError(400, "Не задан вес"); + current.completed = true; + current.completed_at = isoNow(); + current.actual_reps = body.actual_reps; + current.actual_weight_kg = roundExerciseWeight(weight, exercise.name, exercise.kind); + } + + if (exercise.sets.every((s) => s.completed) && exercise.kind === "accessory" && !exercise.weight_locked) { + const allHit = exercise.sets.every( + (s) => s.target_reps == null || (s.actual_reps ?? 0) >= s.target_reps, + ); + exercise.pending_prompt = allHit ? "confirm_easy" : "choose_weight_missed"; + w.rest_started_at = null; + await db.workouts.put(w); + return this.workout(id); + } + + const done = advancePointer(w); + if (done) { + w.status = "completed"; + w.completed_at = isoNow(); + w.rest_started_at = null; + await db.workouts.put(w); + await maybeFinishCycle(w); + } else { + w.rest_started_at = isoNow(); + await db.workouts.put(w); + } + return this.workout(id); + }, + + async prompt( + workoutId: number, + exerciseId: number, + body: { + choice: "easy_yes" | "easy_no"; + weight_kg?: number; + increase_kg?: number; + decrease_kg?: number; + }, + ) { + const w = await getWorkout(workoutId); + if (w.status !== "in_progress") throw new ApiError(400, "Тренировка не активна"); + const exercise = w.exercises.find((e) => e.id === exerciseId); + if (!exercise) throw new ApiError(404, "Упражнение не найдено"); + if (!exercise.pending_prompt) throw new ApiError(400, "Нет активного вопроса"); + + const today = w.scheduled_date; + const currentWeight = exercise.prescribed_weight_kg; + const step = weightStepFor(exercise.name, exercise.kind); + const prompt = exercise.pending_prompt; + + if (prompt === "confirm_easy") { + if (body.choice === "easy_no") { + if (currentWeight == null) throw new ApiError(400, "Вес не задан"); + await applyAccessoryWeightToFuture({ + cycleNumber: w.cycle_number, + exerciseName: exercise.name, + weightKg: currentWeight, + fromDate: today, + lock: true, + }); + exercise.weight_locked = true; + exercise.pending_prompt = null; + } else { + exercise.pending_prompt = "choose_weight_easy"; + await db.workouts.put(w); + return this.workout(workoutId); + } + } else if (prompt === "choose_weight_easy") { + if (currentWeight == null) throw new ApiError(400, "Текущий вес не задан"); + if (body.increase_kg == null || body.increase_kg <= 0) { + throw new ApiError(400, "Укажите, на сколько поднять вес"); + } + let bump = roundToStep(body.increase_kg, step); + if (bump < step) bump = step; + let weight = roundToStep(currentWeight + bump, step); + if (weight <= currentWeight) weight = roundToStep(currentWeight + step, step); + await applyAccessoryWeightToFuture({ + cycleNumber: w.cycle_number, + exerciseName: exercise.name, + weightKg: weight, + fromDate: addDaysIso(today, 1), + lock: false, + }); + exercise.pending_prompt = null; + } else if (prompt === "choose_weight_missed") { + if (currentWeight == null) throw new ApiError(400, "Текущий вес не задан"); + if (body.decrease_kg == null || body.decrease_kg <= 0) { + throw new ApiError(400, "Укажите, на сколько снизить вес"); + } + let drop = roundToStep(body.decrease_kg, step); + if (drop < step) drop = step; + let weight = roundToStep(currentWeight - drop, step); + if (weight >= currentWeight) weight = roundToStep(currentWeight - step, step); + if (weight < ACCESSORY_STEP_KG) weight = ACCESSORY_STEP_KG; + await applyAccessoryWeightToFuture({ + cycleNumber: w.cycle_number, + exerciseName: exercise.name, + weightKg: weight, + fromDate: addDaysIso(today, 1), + lock: false, + }); + exercise.pending_prompt = null; + } else { + throw new ApiError(400, "Неизвестный вопрос"); + } + + const done = advancePointer(w); + if (done) { + w.status = "completed"; + w.completed_at = isoNow(); + w.rest_started_at = null; + await db.workouts.put(w); + await maybeFinishCycle(w); + } else { + w.rest_started_at = isoNow(); + await db.workouts.put(w); + } + return this.workout(workoutId); + }, + + async startRest(restDays: number) { + if (restDays < 3 || restDays > 5) throw new ApiError(400, "Отдых от 3 до 5 дней"); + const p = await ensureProfile(); + const available = new Date(Date.now() + restDays * 86400_000).toISOString(); + p.phase = "rest"; + p.probe_available_at = available; + p.rest_until = available; + await db.profile.put(p); + return profileView(p); + }, + + async probe(body: { one_rm_kg: number; start_date: string; training_weekdays?: number[] }) { + const p = await ensureProfile(); + if (p.phase === "rest" && p.probe_available_at && Date.now() < new Date(p.probe_available_at).getTime()) { + throw new ApiError(400, "Ещё рано для проходки"); + } + const weekdays = body.training_weekdays || p.training_weekdays; + if (!weekdays) throw new ApiError(400, "Не заданы дни тренировок"); + const oneRm = roundToStep(body.one_rm_kg, 1.25); + const newCycle = p.cycle_number + 1; + p.one_rm_kg = oneRm; + p.training_weekdays = weekdays; + p.start_date = body.start_date; + p.cycle_number = newCycle; + p.phase = "active"; + p.rest_until = null; + p.probe_available_at = null; + p.onboarded = true; + await db.profile.put(p); + await generateSchedule({ + oneRmKg: oneRm, + trainingWeekdays: weekdays, + startDate: body.start_date, + cycleNumber: newCycle, + }); + return profileView(p); + }, + + async export() { + const p = await ensureProfile(); + const workouts = await db.workouts.toArray(); + const accessories = await db.accessories.toArray(); + return { + version: 1, + exported_at: isoNow(), + data: { + profile: { + one_rm_kg: p.one_rm_kg, + training_weekdays: p.training_weekdays, + start_date: p.start_date, + onboarded: p.onboarded, + phase: p.phase, + rest_until: p.rest_until, + probe_available_at: p.probe_available_at, + cycle_number: p.cycle_number, + }, + workouts, + accessories, + }, + }; + }, + + async import(payload: { version: number; data: unknown }) { + if (payload.version !== 1) throw new ApiError(400, "Неподдерживаемая версия экспорта"); + const data = payload.data as { + profile?: Record; + workouts?: StoredWorkout[]; + accessories?: { cycle_number: number; exercise_name: string; weight_kg: number | null; locked: boolean; pending_weight_kg: number | null }[]; + }; + await db.workouts.clear(); + await db.accessories.clear(); + const p = await ensureProfile(); + const pr = data.profile || {}; + p.one_rm_kg = (pr.one_rm_kg as number) ?? null; + p.training_weekdays = (pr.training_weekdays as number[]) ?? null; + p.start_date = (pr.start_date as string) ?? null; + p.onboarded = Boolean(pr.onboarded); + p.phase = (pr.phase as string) || "active"; + p.rest_until = (pr.rest_until as string) ?? null; + p.probe_available_at = (pr.probe_available_at as string) ?? null; + p.cycle_number = Number(pr.cycle_number || 1); + await db.profile.put(p); + for (const a of data.accessories || []) { + await db.accessories.add({ + cycle_number: a.cycle_number, + exercise_name: a.exercise_name, + weight_kg: a.weight_kg, + locked: a.locked, + pending_weight_kg: a.pending_weight_kg, + }); + } + for (const w of data.workouts || []) { + await db.workouts.put(w); + } + return { message: "Данные импортированы" }; + }, + + async reset() { + await db.workouts.clear(); + await db.accessories.clear(); + const p = await ensureProfile(); + p.one_rm_kg = null; + p.training_weekdays = null; + p.start_date = null; + p.onboarded = false; + p.phase = "active"; + p.rest_until = null; + p.probe_available_at = null; + p.cycle_number = 1; + await db.profile.put(p); + return { message: "Прогресс удалён. Пройдите онбординг заново." }; + }, +}; diff --git a/frontend/src/local/db.ts b/frontend/src/local/db.ts new file mode 100644 index 0000000..1dc2c59 --- /dev/null +++ b/frontend/src/local/db.ts @@ -0,0 +1,51 @@ +import Dexie, { type Table } from "dexie"; +import type { MetaRow, StoredAccessory, StoredAuth, StoredProfile, StoredWorkout } from "./types"; + +export class BenchpressDB extends Dexie { + auth!: Table; + profile!: Table; + workouts!: Table; + accessories!: Table; + meta!: Table; + + constructor() { + super("benchpress_secret_v2"); + this.version(1).stores({ + auth: "id", + profile: "id", + workouts: "++id, cycle_number, scheduled_date, status", + accessories: "++id, [cycle_number+exercise_name], cycle_number", + meta: "key", + }); + } +} + +export const db = new BenchpressDB(); + +export async function nextId(key: string): Promise { + return db.transaction("rw", db.meta, async () => { + const row = await db.meta.get(key); + const next = (row?.value ?? 0) + 1; + await db.meta.put({ key, value: next }); + return next; + }); +} + +export async function ensureProfile(): Promise { + let p = await db.profile.get(1); + if (!p) { + p = { + id: 1, + one_rm_kg: null, + training_weekdays: null, + start_date: null, + onboarded: false, + phase: "active", + rest_until: null, + probe_available_at: null, + cycle_number: 1, + }; + await db.profile.put(p); + } + return p; +} diff --git a/frontend/src/local/reschedule.ts b/frontend/src/local/reschedule.ts new file mode 100644 index 0000000..a09420f --- /dev/null +++ b/frontend/src/local/reschedule.ts @@ -0,0 +1,132 @@ +export type Slot = { + id: number; + week_number: number; + day_index: number; + scheduled_date: string; + status: string; +}; + +function order(w: Slot): number { + return w.week_number * 10 + w.day_index; +} + +function addDays(iso: string, days: number): string { + const d = new Date(iso + "T12:00:00"); + d.setDate(d.getDate() + days); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; +} + +function weekdayMon(iso: string): number { + const js = new Date(iso + "T12:00:00").getDay(); + return js === 0 ? 6 : js - 1; +} + +function stepToTrainingDay(iso: string, weekdays: number[], direction: 1 | -1): string { + const selected = new Set(weekdays); + let cursor = addDays(iso, direction); + for (let i = 0; i < 28; i++) { + if (selected.has(weekdayMon(cursor))) return cursor; + cursor = addDays(cursor, direction); + } + throw new Error("Не удалось найти тренировочный день"); +} + +function firstTrainingOnOrAfter(iso: string, weekdays: number[]): string { + if (new Set(weekdays).has(weekdayMon(iso))) return iso; + return stepToTrainingDay(addDays(iso, -1), weekdays, 1); +} + +function isOpen(w: Slot): boolean { + return w.status !== "completed"; +} + +function todayIso(): string { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + +function nextTraining(after: string, weekdays: number[], taken: Set): string { + let date = stepToTrainingDay(after, weekdays, 1); + while (taken.has(date)) date = stepToTrainingDay(date, weekdays, 1); + return date; +} + +function placeInOrder(desired: string, previous: string | undefined, weekdays: number[], taken: Set): string { + let date = desired; + if (previous && date <= previous) date = nextTraining(previous, weekdays, taken); + while (taken.has(date)) date = nextTraining(date, weekdays, taken); + return date; +} + +export function planMove(workouts: Slot[], movedId: number, newDate: string, weekdays: number[]): Map { + const moved = workouts.find((w) => w.id === movedId); + if (!moved) throw new Error("Тренировка не найдена"); + if (!isOpen(moved)) throw new Error("Выполненную тренировку переносить нельзя"); + if (!/^\d{4}-\d{2}-\d{2}$/.test(newDate)) throw new Error("Некорректная дата"); + if (newDate < todayIso()) throw new Error("В закрытый период переносить нельзя"); + + const sorted = [...workouts].sort((a, b) => order(a) - order(b) || a.id - b.id); + const completed = sorted.filter((w) => !isOpen(w)); + if (completed.some((w) => w.scheduled_date === newDate)) { + throw new Error("На эту дату уже есть выполненная тренировка"); + } + const earlierDone = completed.filter((w) => order(w) < order(moved)); + const laterDone = completed.filter((w) => order(w) > order(moved)); + const lastEarlier = earlierDone.at(-1); + const firstLater = laterDone[0]; + if (lastEarlier && newDate <= lastEarlier.scheduled_date) { + throw new Error("Дата должна быть позже уже выполненной тренировки"); + } + if (firstLater && newDate >= firstLater.scheduled_date) { + throw new Error("Дата должна быть раньше уже выполненной следующей тренировки"); + } + + const taken = new Set(completed.map((w) => w.scheduled_date)); + const assigned = new Map(); + let index = 0; + while (index < sorted.length) { + if (!isOpen(sorted[index])) { + index += 1; + continue; + } + const start = index; + while (index < sorted.length && isOpen(sorted[index])) index += 1; + const segment = sorted.slice(start, index); + const prevAnchor = start > 0 ? sorted[start - 1] : undefined; + const nextAnchor = index < sorted.length ? sorted[index] : undefined; + const proposed = segment.map((w) => (w.id === movedId ? newDate : w.scheduled_date)).sort(); + const dates: string[] = []; + for (const raw of proposed) { + const date = placeInOrder(raw, dates.at(-1), weekdays, taken); + if (prevAnchor && date <= prevAnchor.scheduled_date) { + throw new Error("Дата должна быть позже уже выполненной тренировки"); + } + if (nextAnchor && date >= nextAnchor.scheduled_date) { + throw new Error("Дата должна быть раньше уже выполненной следующей тренировки"); + } + dates.push(date); + taken.add(date); + } + segment.forEach((w, slot) => assigned.set(w.id, dates[slot])); + } + + return assigned; +} + +export function planWeekdayChange(workouts: Slot[], weekdays: number[], startFrom: string): Map { + const open = workouts.filter(isOpen).sort((a, b) => order(a) - order(b)); + const taken = new Set(workouts.filter((w) => !isOpen(w)).map((w) => w.scheduled_date)); + const assigned = new Map(); + let cursor = startFrom; + for (const item of open) { + let date = firstTrainingOnOrAfter(cursor, weekdays); + while (taken.has(date)) date = stepToTrainingDay(date, weekdays, 1); + assigned.set(item.id, date); + taken.add(date); + cursor = addDays(date, 1); + } + return assigned; +} diff --git a/frontend/src/local/schedule.ts b/frontend/src/local/schedule.ts new file mode 100644 index 0000000..8c3776e --- /dev/null +++ b/frontend/src/local/schedule.ts @@ -0,0 +1,217 @@ +import programData from "../data/program.json" with { type: "json" }; +import { benchWeight, weightStepFor } from "./weights"; +import type { StoredAccessory, StoredExercise, StoredSet, StoredWorkout } from "./types"; +import { db, nextId } from "./db"; + +type ProgramSet = { percent?: number; reps?: number; type?: string }; +type ProgramExercise = { name: string; kind: string; sets: ProgramSet[] }; +type ProgramDay = { template_day: string; label: string; exercises: ProgramExercise[] }; +type ProgramWeek = { week: number; days: ProgramDay[] }; + +const program = programData as { weeks: ProgramWeek[] }; + +export function getProgram(): { weeks: ProgramWeek[] } { + return program; +} + +export function nextDatesForWeekdays(startIso: string, weekdays: number[], count: number): string[] { + const selected = new Set(weekdays); + const dates: string[] = []; + const cursor = new Date(startIso + "T12:00:00"); + for (let i = 0; i < 400 && dates.length < count; i++) { + const jsDay = cursor.getDay(); + const monBased = jsDay === 0 ? 6 : jsDay - 1; + if (selected.has(monBased)) { + const y = cursor.getFullYear(); + const m = String(cursor.getMonth() + 1).padStart(2, "0"); + const d = String(cursor.getDate()).padStart(2, "0"); + dates.push(`${y}-${m}-${d}`); + } + cursor.setDate(cursor.getDate() + 1); + } + if (dates.length < count) throw new Error("Не удалось построить расписание"); + return dates; +} + +async function getAccessory( + cycle: number, + name: string, +): Promise { + return db.accessories.where({ cycle_number: cycle, exercise_name: name }).first(); +} + +export async function generateSchedule(opts: { + oneRmKg: number; + trainingWeekdays: number[]; + startDate: string; + cycleNumber: number; +}): Promise { + const prog = getProgram(); + const totalDays = prog.weeks.reduce((n, w) => n + w.days.length, 0); + const dates = nextDatesForWeekdays(opts.startDate, opts.trainingWeekdays, totalDays); + let dateIdx = 0; + const created: StoredWorkout[] = []; + + for (const week of prog.weeks) { + for (let dayIndex = 0; dayIndex < week.days.length; dayIndex++) { + const day = week.days[dayIndex]; + const scheduled = dates[dateIdx++]; + const workoutId = await nextId("workout"); + const exercises: StoredExercise[] = []; + + for (let order = 0; order < day.exercises.length; order++) { + const ex = day.exercises[order]; + let needsWeight = false; + let prescribed: number | null = null; + let locked = false; + + if (ex.kind === "accessory") { + const st = await getAccessory(opts.cycleNumber, ex.name); + if (st?.locked && st.weight_kg != null) { + prescribed = st.weight_kg; + locked = true; + } else if (st?.pending_weight_kg != null) { + prescribed = st.pending_weight_kg; + } else if (st?.weight_kg != null) { + prescribed = st.weight_kg; + } else { + needsWeight = true; + } + } + + const exId = await nextId("exercise"); + const sets: StoredSet[] = []; + for (let si = 0; si < ex.sets.length; si++) { + const s = ex.sets[si]; + const setId = await nextId("set"); + if (ex.kind === "bench") { + sets.push({ + id: setId, + set_index: si, + target_percent: s.percent ?? null, + target_reps: s.reps ?? null, + target_weight_kg: benchWeight(opts.oneRmKg, s.percent ?? 0), + is_failure: false, + completed: false, + actual_reps: null, + actual_weight_kg: null, + completed_at: null, + }); + } else if (ex.kind === "failure") { + sets.push({ + id: setId, + set_index: si, + target_percent: null, + target_reps: null, + target_weight_kg: null, + is_failure: true, + completed: false, + actual_reps: null, + actual_weight_kg: null, + completed_at: null, + }); + } else { + sets.push({ + id: setId, + set_index: si, + target_percent: null, + target_reps: s.reps ?? null, + target_weight_kg: prescribed, + is_failure: false, + completed: false, + actual_reps: null, + actual_weight_kg: null, + completed_at: null, + }); + } + } + + exercises.push({ + id: exId, + order_index: order, + name: ex.name, + kind: ex.kind, + prescribed_weight_kg: prescribed, + weight_locked: locked, + needs_weight_input: needsWeight, + pending_prompt: null, + sets, + }); + } + + const workout: StoredWorkout = { + id: workoutId, + cycle_number: opts.cycleNumber, + week_number: week.week, + day_index: dayIndex, + scheduled_date: scheduled, + status: "planned", + started_at: null, + paused_at: null, + completed_at: null, + current_exercise_index: 0, + current_set_index: 0, + rest_started_at: null, + exercises, + }; + await db.workouts.put(workout); + created.push(workout); + } + } + return created; +} + +export async function applyAccessoryWeightToFuture(opts: { + cycleNumber: number; + exerciseName: string; + weightKg: number; + fromDate: string; + lock: boolean; +}): Promise { + let st = await getAccessory(opts.cycleNumber, opts.exerciseName); + if (!st) { + st = { + cycle_number: opts.cycleNumber, + exercise_name: opts.exerciseName, + weight_kg: null, + locked: false, + pending_weight_kg: null, + }; + } + if (opts.lock) { + st.weight_kg = opts.weightKg; + st.locked = true; + st.pending_weight_kg = null; + } else { + st.weight_kg = opts.weightKg; + st.pending_weight_kg = opts.weightKg; + st.locked = false; + } + if (st.id != null) await db.accessories.put(st); + else await db.accessories.add(st); + + const workouts = await db.workouts.where("cycle_number").equals(opts.cycleNumber).toArray(); + for (const w of workouts) { + if (w.status === "completed" || w.scheduled_date < opts.fromDate) continue; + let changed = false; + for (const ex of w.exercises) { + if (ex.name !== opts.exerciseName || ex.kind !== "accessory") continue; + if (ex.sets.length > 0 && ex.sets.every((s) => s.completed)) continue; + ex.prescribed_weight_kg = opts.weightKg; + ex.weight_locked = opts.lock; + ex.needs_weight_input = false; + for (const s of ex.sets) { + if (!s.completed) s.target_weight_kg = opts.weightKg; + } + changed = true; + } + if (changed) await db.workouts.put(w); + } +} + +export function todayIso(): string { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + +export { weightStepFor }; diff --git a/frontend/src/local/types.ts b/frontend/src/local/types.ts new file mode 100644 index 0000000..113ab93 --- /dev/null +++ b/frontend/src/local/types.ts @@ -0,0 +1,71 @@ +export type StoredSet = { + id: number; + set_index: number; + target_percent: number | null; + target_reps: number | null; + target_weight_kg: number | null; + is_failure: boolean; + completed: boolean; + actual_reps: number | null; + actual_weight_kg: number | null; + completed_at: string | null; +}; + +export type StoredExercise = { + id: number; + order_index: number; + name: string; + kind: string; + prescribed_weight_kg: number | null; + weight_locked: boolean; + needs_weight_input: boolean; + pending_prompt: string | null; + sets: StoredSet[]; +}; + +export type StoredWorkout = { + id: number; + cycle_number: number; + week_number: number; + day_index: number; + scheduled_date: string; + status: string; + started_at: string | null; + paused_at: string | null; + completed_at: string | null; + current_exercise_index: number; + current_set_index: number; + rest_started_at: string | null; + exercises: StoredExercise[]; +}; + +export type StoredProfile = { + id: number; + one_rm_kg: number | null; + training_weekdays: number[] | null; + start_date: string | null; + onboarded: boolean; + phase: string; + rest_until: string | null; + probe_available_at: string | null; + cycle_number: number; +}; + +export type StoredAuth = { + id: number; + password_hash: string; +}; + +export type StoredAccessory = { + id?: number; + cycle_number: number; + exercise_name: string; + weight_kg: number | null; + locked: boolean; + pending_weight_kg: number | null; +}; + +export type MetaRow = { + key: string; + value: number; +}; diff --git a/frontend/src/local/weights.ts b/frontend/src/local/weights.ts new file mode 100644 index 0000000..c8c3026 --- /dev/null +++ b/frontend/src/local/weights.ts @@ -0,0 +1,21 @@ +export const PLATE_STEP_KG = 1.25; +export const ACCESSORY_STEP_KG = 0.5; +export const PAUSE_LIMIT_MINUTES = 15; + +export function weightStepFor(name: string, kind: string): number { + if (kind === "bench" || name === "Жим") return PLATE_STEP_KG; + return ACCESSORY_STEP_KG; +} + +export function roundToStep(weightKg: number, step: number): number { + if (weightKg <= 0) return 0; + return Math.round(Math.round(weightKg / step) * step * 100) / 100; +} + +export function roundExerciseWeight(weightKg: number, name: string, kind: string): number { + return roundToStep(weightKg, weightStepFor(name, kind)); +} + +export function benchWeight(oneRmKg: number, percent: number): number { + return roundToStep(oneRmKg * (percent / 100), PLATE_STEP_KG); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..b3c2e37 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..cbe0b02 --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,314 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { api, formatDhms, type Profile, type WorkoutSummary, WEEKDAY_LABELS } from "../api"; +import { AppCalendar } from "../components/AppCalendar"; + +function startOfMonth(year: number, month: number) { + return new Date(year, month - 1, 1); +} + +function daysInMonth(year: number, month: number) { + return new Date(year, month, 0).getDate(); +} + +function iso(d: Date) { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; +} + +function weekStrip(workouts: WorkoutSummary[], todayIso: string): WorkoutSummary[] { + const today = new Date(todayIso); + const day = today.getDay(); + const mondayOffset = day === 0 ? -6 : 1 - day; + const monday = new Date(today); + monday.setDate(today.getDate() + mondayOffset); + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + const from = iso(monday); + const to = iso(sunday); + return workouts.filter((w) => w.scheduled_date >= from && w.scheduled_date <= to); +} + +export function HomePage() { + const navigate = useNavigate(); + const now = new Date(); + const [year, setYear] = useState(now.getFullYear()); + const [month, setMonth] = useState(now.getMonth() + 1); + const [workouts, setWorkouts] = useState([]); + const [allWorkouts, setAllWorkouts] = useState([]); + const [profile, setProfile] = useState(null); + const [error, setError] = useState(""); + const [restDays, setRestDays] = useState(3); + const [probeRm, setProbeRm] = useState(""); + const [probeStart, setProbeStart] = useState(() => iso(new Date())); + const [countdown, setCountdown] = useState(0); + const [drag, setDrag] = useState<{ + id: number; + from: string; + originX: number; + originY: number; + x: number; + y: number; + hover: string | null; + moved: boolean; + } | null>(null); + const dragRef = useRef(drag); + const suppressClick = useRef(false); + + const load = async () => { + try { + const [cal, upcoming] = await Promise.all([api.calendar(year, month), api.upcoming()]); + setWorkouts(cal.workouts); + setAllWorkouts(upcoming); + setProfile(cal.profile); + setCountdown(cal.profile.rest_countdown_seconds || 0); + if (cal.profile.one_rm_kg) setProbeRm(String(cal.profile.one_rm_kg)); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка загрузки"); + } + }; + + useEffect(() => { + load(); + }, [year, month]); + + useEffect(() => { + if (!profile || profile.phase !== "rest" || profile.can_probe) return; + const t = setInterval(() => setCountdown((c) => Math.max(0, c - 1)), 1000); + return () => clearInterval(t); + }, [profile?.phase, profile?.can_probe]); + + const byDate = useMemo(() => { + const map = new Map(); + for (const w of workouts) map.set(w.scheduled_date, w); + return map; + }, [workouts]); + + const todayIso = iso(new Date()); + const strip = weekStrip(allWorkouts, todayIso); + const firstWeekday = (startOfMonth(year, month).getDay() + 6) % 7; // Mon=0 + const totalDays = daysInMonth(year, month); + + const shiftMonth = (delta: number) => { + const d = new Date(year, month - 1 + delta, 1); + setYear(d.getFullYear()); + setMonth(d.getMonth() + 1); + }; + + const statusClass = (s: string) => { + if (s === "completed") return "done"; + if (s === "in_progress" || s === "paused") return "active"; + if (s === "abandoned") return "missed"; + return "planned"; + }; + + return ( +
+
+
+

Benchpress Secret

+ {profile?.one_rm_kg != null &&

1ПМ: {profile.one_rm_kg} кг · цикл {profile.cycle_number}

} +
+ + + +
+ + {profile && (profile.phase === "rest" || profile.phase === "probe" || profile.can_probe) && ( +
+ {!profile.can_probe && profile.phase === "rest" && profile.probe_available_at == null && ( +
+

Цикл завершён

+

Выберите длительность отдыха (3–5 дней)

+
+ {[3, 4, 5].map((d) => ( + + ))} +
+ +
+ )} + + {!profile.can_probe && profile.probe_available_at && ( +
+

Отдых

+

До проходки: {formatDhms(countdown)}

+
+ )} + + {profile.can_probe && ( +
+

Сделай проходку

+

Введи новый одноповторный максимум

+ +

Дата старта нового цикла

+ + +
+ )} +
+ )} + +
+

Эта неделя

+
+ {strip.length === 0 &&

Нет тренировок на этой неделе

} + {strip.map((w) => ( + + ))} +
+
+ +
+
+ +

+ {now.toLocaleString("ru-RU", { month: "long" }) && + new Date(year, month - 1, 1).toLocaleString("ru-RU", { month: "long", year: "numeric" })} +

+ +
+
+ {WEEKDAY_LABELS.map((d) => ( +
{d}
+ ))} +
+
+ {Array.from({ length: firstWeekday }).map((_, i) => ( +
+ ))} + {Array.from({ length: totalDays }).map((_, i) => { + const day = i + 1; + const key = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const w = byDate.get(key); + const movable = !!w && w.status !== "completed" && w.status !== "abandoned"; + return ( + + ); + })} +
+ {drag?.moved && ( +
+ {Number(drag.from.slice(8))} +
+ )} +
+ + {error &&

{error}

} +
+ ); +} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..9f50630 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,67 @@ +import { useState, type FormEvent } from "react"; +import { useAuth } from "../auth"; + +export function LoginPage() { + const { hasPassword, setup, login } = useAuth(); + const [password, setPassword] = useState(""); + const [password2, setPassword2] = useState(""); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(""); + setBusy(true); + try { + if (!hasPassword) { + if (password.length < 4) throw new Error("Минимум 4 символа"); + if (password !== password2) throw new Error("Пароли не совпадают"); + await setup(password); + } else { + await login(password); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка"); + } finally { + setBusy(false); + } + }; + + return ( +
+
+

Benchpress Secret

+

Программа жима лёжа на 5 недель

+
+
+

{hasPassword ? "Вход" : "Создайте пароль"}

+ + {!hasPassword && ( + + )} + {error &&

{error}

} + +
+
+ ); +} diff --git a/frontend/src/pages/OnboardingPage.tsx b/frontend/src/pages/OnboardingPage.tsx new file mode 100644 index 0000000..e91c11e --- /dev/null +++ b/frontend/src/pages/OnboardingPage.tsx @@ -0,0 +1,207 @@ +import { useMemo, useState, type FormEvent } from "react"; +import { api, WEEKDAY_FULL } from "../api"; +import { AppCalendar } from "../components/AppCalendar"; +import { useAuth } from "../auth"; + +function daysOk(selected: number[]): boolean { + if (selected.length !== 3) return false; + if (new Set(selected).size !== 3) return false; + const sorted = [...selected].sort((a, b) => a - b); + for (let i = 0; i < 3; i++) { + const a = sorted[i]; + const b = sorted[(i + 1) % 3]; + const gap = i < 2 ? b - a - 1 : a + 7 - b - 1; + if (gap < 1) return false; + } + return true; +} + +function roundToPlate(value: number): number { + return Math.round(Math.round(value / 1.25) * 1.25 * 100) / 100; +} + +function formatRm(value: number): string { + return String(value); +} + +export function OnboardingPage() { + const { refresh } = useAuth(); + const [step, setStep] = useState(0); + const [oneRm, setOneRm] = useState(100); + const [oneRmText, setOneRmText] = useState("100"); + const [days, setDays] = useState([]); + const [startDate, setStartDate] = useState(() => new Date().toISOString().slice(0, 10)); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + + const daysValid = useMemo(() => daysOk(days), [days]); + const canFinish = daysValid && oneRm > 0 && !!startDate; + + const toggleDay = (d: number) => { + setDays((prev) => { + if (prev.includes(d)) return prev.filter((x) => x !== d); + if (prev.length >= 3) return prev; + return [...prev, d]; + }); + }; + + const commitRm = (raw = oneRmText) => { + const n = Number(raw.trim().replace(",", ".")); + const rounded = !Number.isFinite(n) || n <= 0 ? oneRm : Math.max(1.25, roundToPlate(n)); + setOneRm(rounded); + setOneRmText(formatRm(rounded)); + return rounded; + }; + + const bumpRm = (delta: number) => { + const base = commitRm(); + const next = Math.max(1.25, roundToPlate(base + delta)); + setOneRm(next); + setOneRmText(formatRm(next)); + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!canFinish) return; + setBusy(true); + setError(""); + try { + await api.onboarding({ + one_rm_kg: commitRm(), + training_weekdays: days, + start_date: startDate, + }); + await refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка"); + } finally { + setBusy(false); + } + }; + + return ( +
+
+

Benchpress Secret

+

Шаг {step + 1} из 3 — настройка программы

+
+ +
+ {["1ПМ", "Дни", "Старт"].map((label, i) => ( + + ))} +
+ +
+ {step === 0 && ( +
+

Максимум на 1 повторение

+

Жим лёжа, кг. Шаг блинов 1.25

+
+ +
+ setOneRmText(e.target.value)} + onBlur={() => commitRm()} + required + /> + кг +
+ +
+ +
+ )} + + {step === 1 && ( +
+

Выберите 3 дня

+

Между соседними днями нужен минимум 1 день перерыва

+
+ {WEEKDAY_FULL.map((label, idx) => { + const on = days.includes(idx); + return ( + + ); + })} +
+

Выбрано: {days.length} / 3

+ {days.length === 3 && !daysValid && ( +

Между днями нужен минимум 1 день перерыва

+ )} +
+ + +
+
+ )} + + {step === 2 && ( +
+

Дата старта

+

С этой даты построим расписание на 5 недель

+

Первый день программы

+ +
+

+ 1ПМ: {oneRm} кг +

+

+ Дни:{" "} + {days + .slice() + .sort((a, b) => a - b) + .map((d) => WEEKDAY_FULL[d]) + .join(", ")} +

+
+ {error &&

{error}

} +
+ + +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..75bff3c --- /dev/null +++ b/frontend/src/pages/SettingsPage.tsx @@ -0,0 +1,209 @@ +import { useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { api, WEEKDAY_FULL, type Profile } from "../api"; +import { useAuth } from "../auth"; + +function daysOk(selected: number[]): boolean { + if (selected.length !== 3) return false; + if (new Set(selected).size !== 3) return false; + const sorted = [...selected].sort((a, b) => a - b); + for (let i = 0; i < 3; i++) { + const a = sorted[i]; + const b = sorted[(i + 1) % 3]; + const gap = i < 2 ? b - a - 1 : a + 7 - b - 1; + if (gap < 1) return false; + } + return true; +} + +function formatStart(iso: string): string { + const d = new Date(iso + "T12:00:00"); + return d + .toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" }) + .replace(/\s*г\.?$/, ""); +} + +export function SettingsPage() { + const { refresh } = useAuth(); + const navigate = useNavigate(); + const fileRef = useRef(null); + const [profile, setProfile] = useState(null); + const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + const [editingDays, setEditingDays] = useState(false); + const [days, setDays] = useState([]); + const [daysBusy, setDaysBusy] = useState(false); + + useEffect(() => { + api + .profile() + .then((p) => { + setProfile(p); + setDays(p.training_weekdays ?? []); + }) + .catch(() => undefined); + }, []); + + const toggleDay = (d: number) => { + setDays((prev) => { + if (prev.includes(d)) return prev.filter((x) => x !== d); + if (prev.length >= 3) return prev; + return [...prev, d]; + }); + }; + + const saveDays = async () => { + if (!daysOk(days)) { + setError("Между днями нужен минимум 1 день перерыва"); + return; + } + setDaysBusy(true); + setError(""); + try { + const next = await api.updateTrainingDays(days); + setProfile(next); + setDays(next.training_weekdays ?? days); + setEditingDays(false); + await refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка"); + } finally { + setDaysBusy(false); + } + }; + + const exportData = async () => { + setError(""); + try { + const data = await api.export(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `benchpress-secret-${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(url); + setMessage("Экспорт скачан"); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка экспорта"); + } + }; + + const onImport = async (file: File) => { + setError(""); + try { + const text = await file.text(); + const payload = JSON.parse(text); + const res = await api.import({ version: payload.version ?? 1, data: payload.data ?? payload }); + setMessage(res.message); + await refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка импорта"); + } + }; + + const reset = async () => { + if ( + !confirm( + "Данные будут удалены и прогресс потерян. После этого снова выберете 1ПМ, дни и дату старта. Продолжить?", + ) + ) { + return; + } + try { + await api.reset(); + await refresh(); + navigate("/"); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка сброса"); + } + }; + + const daysLabel = + profile?.training_weekdays + ?.slice() + .sort((a, b) => a - b) + .map((d) => WEEKDAY_FULL[d]) + .join(", ") || "—"; + + return ( +
+
+

Настройки

+
+ +
+
+

+ 1ПМ: {profile?.one_rm_kg ?? "—"} кг +

+

+ Дни: {daysLabel} +

+

+ Старт: {profile?.start_date ? formatStart(profile.start_date) : "—"} +

+
+ + + {editingDays && ( +
+
+ {WEEKDAY_FULL.map((label, idx) => { + const on = days.includes(idx); + return ( + + ); + })} +
+ {days.length === 3 && !daysOk(days) && ( +

Между днями нужен минимум 1 день перерыва

+ )} + +
+ )} + + + + { + const f = e.target.files?.[0]; + if (f) onImport(f); + }} + /> +
+ + {message &&

{message}

} + {error &&

{error}

} + +
+ + +
+
+ ); +} diff --git a/frontend/src/pages/WorkoutPage.tsx b/frontend/src/pages/WorkoutPage.tsx new file mode 100644 index 0000000..fa2fc38 --- /dev/null +++ b/frontend/src/pages/WorkoutPage.tsx @@ -0,0 +1,807 @@ +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { api, formatDateRu, formatDhms, formatKg, formatRestCountdown, type Exercise, type WorkoutDetail, type WorkoutSummary } from "../api"; +import { AppCalendar } from "../components/AppCalendar"; +import { WeightStepper } from "../components/WeightStepper"; + +const ACCESSORY_STEP = 0.5; + +function AccessoryWeightEditor({ + ex, + busy, + onSave, +}: { + ex: Exercise; + busy: boolean; + onSave: (weight: number) => void; +}) { + const step = ex.weight_step_kg || ACCESSORY_STEP; + const [value, setValue] = useState(ex.prescribed_weight_kg ?? step * 20); + + useEffect(() => { + setValue(ex.prescribed_weight_kg ?? step * 20); + }, [ex.id, ex.prescribed_weight_kg, step]); + + return ( +
+ onSave(value)} + > + + + } + /> +
+ ); +} + +function roundToStep(value: number, step: number, min: number): number { + const next = Math.round(value / step) * step; + return Math.max(min, Math.round(next * 100) / 100); +} + +function EditIcon() { + return ( + + ); +} + +function AccessorySetRow({ + workoutId, + exerciseId, + setId, + weight, + reps, + targetReps, + step, + showAdjust, + onError, +}: { + workoutId: number; + exerciseId: number; + setId: number; + weight: number; + reps: number; + targetReps: number | null; + step: number; + showAdjust: boolean; + onError: (message: string) => void; +}) { + const [rowWeight, setRowWeight] = useState(weight); + const [rowReps, setRowReps] = useState(reps); + const [rowBusy, setRowBusy] = useState(false); + + const save = async (nextWeight: number, nextReps: number) => { + const prevWeight = rowWeight; + const prevReps = rowReps; + setRowWeight(nextWeight); + setRowReps(nextReps); + setRowBusy(true); + try { + await api.adjustSet(workoutId, exerciseId, setId, { + actual_weight_kg: nextWeight, + actual_reps: nextReps, + }); + } catch (err) { + setRowWeight(prevWeight); + setRowReps(prevReps); + onError(err instanceof Error ? err.message : "Ошибка"); + } finally { + setRowBusy(false); + } + }; + + return ( +
  • + + {formatKg(rowWeight)} кг × {targetReps} + + {showAdjust && ( + + + кг + + + повт. + + + )} +
  • + ); +} + +function allSetsDone(ex: Exercise): boolean { + return ex.sets.length > 0 && ex.sets.every((s) => s.completed); +} + +function doneCount(ex: Exercise): number { + return ex.sets.filter((s) => s.completed).length; +} + +export function WorkoutPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const workoutId = Number(id); + const [workout, setWorkout] = useState(null); + const [error, setError] = useState(""); + const [reps, setReps] = useState(""); + const [, setWeightValue] = useState(10); + const [adjustAmount, setAdjustAmount] = useState(0.5); + const [restSeconds, setRestSeconds] = useState(0); + const [pauseLeftSeconds, setPauseLeftSeconds] = useState(0); + const [untilWorkoutSeconds, setUntilWorkoutSeconds] = useState(0); + const [dateOpen, setDateOpen] = useState(false); + const [schedule, setSchedule] = useState([]); + const [busy, setBusy] = useState(false); + const [expandedId, setExpandedId] = useState(null); + const [editingId, setEditingId] = useState(null); + const prevExerciseId = useRef(null); + + const applyWorkout = (w: WorkoutDetail) => { + setWorkout(w); + setRestSeconds(w.rest_elapsed_seconds || 0); + if (w.status === "paused" && w.pause_expires_at) { + const left = Math.max(0, Math.floor((new Date(w.pause_expires_at).getTime() - Date.now()) / 1000)); + setPauseLeftSeconds(left); + } else { + setPauseLeftSeconds(0); + } + const ex = w.exercises[w.current_exercise_index]; + if (ex?.kind === "accessory") { + const step = ex.weight_step_kg ?? ACCESSORY_STEP; + if (ex.prescribed_weight_kg != null) { + setWeightValue(ex.prescribed_weight_kg); + } else { + setWeightValue(step * 20); // разумный старт ~10 кг при шаге 0.5 + } + setAdjustAmount(step); + } + const next = ex?.sets.find((s) => !s.completed); + if (next?.target_reps != null) setReps(String(next.target_reps)); + }; + + const load = async () => { + const w = await api.workout(workoutId); + applyWorkout(w); + const cur = w.exercises[w.current_exercise_index]; + if (cur && (w.status === "in_progress" || w.status === "paused")) { + setExpandedId(cur.id); + prevExerciseId.current = cur.id; + } else { + setExpandedId(null); + prevExerciseId.current = null; + } + }; + + const pageRef = useRef(null); + const dragRef = useRef({ x: 0, y: 0, horizontal: false, decided: false }); + const leavingRef = useRef(false); + const location = useLocation(); + const [offset, setOffset] = useState(0); + const [glide, setGlide] = useState(false); + + useEffect(() => { + setEditingId(null); + setDateOpen(false); + load().catch((err) => setError(err instanceof Error ? err.message : "Ошибка")); + api.upcoming().then(setSchedule).catch(() => undefined); + }, [workoutId]); + + useEffect(() => { + if (!workout || workout.status !== "in_progress" || !workout.rest_started_at) return; + const t = setInterval(() => setRestSeconds((s) => s + 1), 1000); + return () => clearInterval(t); + }, [workout?.status, workout?.rest_started_at, workout?.id, workout?.current_set_index]); + + useEffect(() => { + if (!workout || workout.status !== "paused" || !workout.pause_expires_at || !workout.can_resume) return; + const tick = () => { + const left = Math.max(0, Math.floor((new Date(workout.pause_expires_at!).getTime() - Date.now()) / 1000)); + setPauseLeftSeconds(left); + }; + tick(); + const t = setInterval(tick, 1000); + return () => clearInterval(t); + }, [workout?.status, workout?.pause_expires_at, workout?.can_resume, workout?.id]); + + useEffect(() => { + if (!workout || workout.status !== "planned") return; + const tick = () => { + const start = new Date(workout.scheduled_date + "T00:00:00").getTime(); + setUntilWorkoutSeconds(Math.max(0, Math.floor((start - Date.now()) / 1000))); + }; + tick(); + const t = setInterval(tick, 1000); + return () => clearInterval(t); + }, [workout?.status, workout?.scheduled_date, workout?.id]); + + // Auto-expand current exercise when it changes; collapse the previous one + useEffect(() => { + if (!workout) return; + if (workout.status !== "in_progress" && workout.status !== "paused") return; + const cur = workout.exercises[workout.current_exercise_index]; + if (!cur) return; + if (prevExerciseId.current === cur.id) return; + setExpandedId(cur.id); + prevExerciseId.current = cur.id; + }, [workout?.current_exercise_index, workout?.status, workout?.id, workout?.exercises]); + + const currentExercise = workout?.exercises[workout.current_exercise_index]; + const currentSet = useMemo(() => { + if (!workout || !currentExercise) return null; + return currentExercise.sets.find((s) => s.set_index === workout.current_set_index) || null; + }, [workout, currentExercise]); + + const liveStatus = useMemo(() => { + if (!workout) return ""; + if (workout.status === "in_progress" && workout.rest_started_at) { + return `Отдых ${formatDhms(restSeconds)}`; + } + if (workout.status === "paused" && workout.can_resume) { + return `Отдых ${formatDhms(pauseLeftSeconds)}`; + } + if (workout.status === "in_progress") { + return "Идёт тренировка"; + } + if (workout.status === "planned") { + if (workout.can_start) return "Можно начать"; + if (untilWorkoutSeconds > 0) return `Отдых ${formatDhms(untilWorkoutSeconds)}`; + return "Запланирована"; + } + if (workout.status === "completed") return "Выполнена"; + if (workout.status === "abandoned") return "Остановлена"; + if (workout.status === "paused") return "Остановлена"; + return "Запланирована"; + }, [workout, restSeconds, pauseLeftSeconds, untilWorkoutSeconds]); + + const toggleExpanded = (exerciseId: number) => { + setExpandedId((prev) => (prev === exerciseId ? null : exerciseId)); + }; + + const run = async (fn: () => Promise) => { + setBusy(true); + setError(""); + try { + applyWorkout(await fn()); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка"); + await load().catch(() => undefined); + } finally { + setBusy(false); + } + }; + + const neighbor = useMemo(() => { + const idx = schedule.findIndex((w) => w.id === workout?.id); + return { + prev: idx > 0 ? schedule[idx - 1] : null, + next: idx >= 0 && idx < schedule.length - 1 ? schedule[idx + 1] : null, + }; + }, [schedule, workout?.id]); + + const neighborRef = useRef(neighbor); + neighborRef.current = neighbor; + const dateOpenRef = useRef(dateOpen); + dateOpenRef.current = dateOpen; + + useLayoutEffect(() => { + const slide = (location.state as { slide?: "next" | "prev" } | null)?.slide; + if (!slide) { + setGlide(false); + setOffset(0); + return; + } + setGlide(false); + setOffset(slide === "next" ? window.innerWidth : -window.innerWidth); + const frame = requestAnimationFrame(() => { + setGlide(true); + setOffset(0); + }); + return () => cancelAnimationFrame(frame); + }, [workoutId, location.state]); + + useEffect(() => { + const el = pageRef.current; + if (!el) return; + + const start = (e: globalThis.TouchEvent) => { + if (dateOpenRef.current || leavingRef.current) return; + const t = e.changedTouches[0]; + dragRef.current = { x: t.clientX, y: t.clientY, horizontal: false, decided: false }; + }; + + const move = (e: globalThis.TouchEvent) => { + if (dateOpenRef.current || leavingRef.current) return; + const drag = dragRef.current; + if (!drag.x && !drag.y) return; + const t = e.changedTouches[0]; + const dx = t.clientX - drag.x; + const dy = t.clientY - drag.y; + if (!drag.decided) { + if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return; + drag.decided = true; + drag.horizontal = Math.abs(dx) > Math.abs(dy); + } + if (!drag.horizontal) return; + e.preventDefault(); + let shift = dx; + if (shift < 0 && !neighborRef.current.next) shift *= 0.22; + if (shift > 0 && !neighborRef.current.prev) shift *= 0.22; + setGlide(false); + setOffset(shift); + }; + + const end = (e: globalThis.TouchEvent) => { + const drag = dragRef.current; + dragRef.current = { x: 0, y: 0, horizontal: false, decided: false }; + if (!drag.horizontal || dateOpenRef.current || leavingRef.current) { + if (drag.horizontal) { + setGlide(true); + setOffset(0); + } + return; + } + const t = e.changedTouches[0]; + const dx = t.clientX - drag.x; + const target = dx < 0 ? neighborRef.current.next : neighborRef.current.prev; + if (Math.abs(dx) < 70 || !target) { + setGlide(true); + setOffset(0); + return; + } + leavingRef.current = true; + const dir = dx < 0 ? "next" : "prev"; + setGlide(true); + setOffset(dir === "next" ? -window.innerWidth : window.innerWidth); + window.setTimeout(() => { + leavingRef.current = false; + navigate(`/workout/${target.id}`, { state: { slide: dir } }); + }, 230); + }; + + el.addEventListener("touchstart", start, { passive: true }); + el.addEventListener("touchmove", move, { passive: false }); + el.addEventListener("touchend", end); + return () => { + el.removeEventListener("touchstart", start); + el.removeEventListener("touchmove", move); + el.removeEventListener("touchend", end); + }; + }, [workout?.id, navigate]); + + if (!workout) { + return ( +
    +

    {error || "Загрузка…"}

    +
    + ); + } + + return ( +
    +
    +
    + {workout.status === "in_progress" && ( + + )} + {workout.status === "paused" && workout.can_resume && ( + + )} +
    +
    + +
    +

    + Неделя {workout.week_number} · день {workout.day_index + 1} +

    +

    + {formatDateRu(workout.scheduled_date)} + {!(workout.status === "planned" && !workout.can_start && untilWorkoutSeconds > 0) && ` · ${liveStatus}`} +

    + {workout.status === "planned" && !workout.can_start && untilWorkoutSeconds > 0 && ( +
    +

    Отдых

    +

    до следующей тренировки {formatRestCountdown(untilWorkoutSeconds)}

    +
    + )} + {workout.status === "paused" && !workout.can_resume && ( +

    Пауза истекла — тренировка остановлена

    + )} +
    + + {workout.status === "planned" && workout.can_start && ( + + )} + + {dateOpen && workout.status !== "completed" && ( +
    node?.scrollIntoView({ block: "start" })}> +

    Выберите новую дату

    + { + setDateOpen(false); + if (isoDate === workout.scheduled_date) return; + setBusy(true); + setError(""); + api + .moveWorkout(workout.id, isoDate) + .then(() => navigate("/")) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : "Ошибка"); + setBusy(false); + }); + }} + /> +
    + )} + +
    +

    Упражнения

    + {workout.exercises.map((ex, index) => { + const isOpen = expandedId === ex.id; + const isCurrent = + (workout.status === "in_progress" || workout.status === "paused") && + index === workout.current_exercise_index; + const finished = allSetsDone(ex); + + const weightKnown = ex.kind === "accessory" && !ex.needs_weight_input && ex.prescribed_weight_kg != null; + const canEdit = ex.kind === "accessory" && (finished || weightKnown); + const editing = editingId === ex.id; + const step = ex.weight_step_kg || ACCESSORY_STEP; + + return ( +
    +
    + + {canEdit && ( + + )} +
    + + {isOpen && ( +
    + {ex.kind === "accessory" && + workout.status !== "completed" && + !finished && + (ex.needs_weight_input || editing) && ( + + run(async () => { + const next = await api.setWeight(workout.id, ex.id, weight); + setEditingId((current) => (current === ex.id ? null : current)); + return next; + }) + } + /> + )} + {isCurrent && + workout.status === "in_progress" && + currentSet && + !ex.pending_prompt && ( +
    +

    + Подход {currentSet.set_index + 1} / {ex.sets.length} +

    + + {ex.kind === "bench" && ( +
    +
    + {formatKg(currentSet.target_weight_kg)} + кг +
    +
    + {currentSet.target_reps} + повт. +
    +
    + )} + + {ex.kind === "failure" && ( +
    +
    + В отказ +
    +
    + )} + + {ex.kind === "accessory" && !ex.needs_weight_input && ( + <> +
    +
    + {formatKg(currentSet.target_weight_kg)} + кг +
    +
    + {currentSet.target_reps} + цель +
    +
    + + + )} + + {!ex.needs_weight_input && ( + + )} +
    + )} + + {isCurrent && workout.status === "in_progress" && ex.pending_prompt && ( +
    + {ex.pending_prompt === "confirm_easy" && ( + <> +

    Было слишком легко?

    +
    + + +
    + + )} + {ex.pending_prompt === "choose_weight_easy" && ( + <> +

    На сколько поднять вес?

    +

    + Сейчас: {formatKg(ex.prescribed_weight_kg)} кг → будет{" "} + {formatKg((ex.prescribed_weight_kg ?? 0) + adjustAmount)} кг +

    + + Шаг 0.5 кг. Новый вес не будет меньше текущего. + + + )} + {ex.pending_prompt === "choose_weight_missed" && ( + <> +

    На сколько снизить вес?

    +

    + Сейчас: {formatKg(ex.prescribed_weight_kg)} кг → будет{" "} + {formatKg(Math.max(ACCESSORY_STEP, (ex.prescribed_weight_kg ?? 0) - adjustAmount))}{" "} + кг +

    + + Шаг 0.5 кг. Новый вес будет меньше текущего. + + + )} +
    + )} + +
      + {ex.sets.map((s) => { + const showAdjust = editing && finished && s.completed && ex.kind === "accessory"; + if (showAdjust) { + return ( + + ); + } + return ( +
    • + + {s.is_failure ? ( + <>В отказ {s.completed ? "✓" : ""} + ) : ex.kind === "bench" ? ( + <> + {formatKg(s.target_weight_kg)} кг × {s.target_reps} + {s.completed ? " ✓" : ""} + + ) : ( + <> + {formatKg(s.target_weight_kg ?? ex.prescribed_weight_kg)} кг × {s.target_reps} + + )} + +
    • + ); + })} +
    +
    + )} +
    + ); + })} +
    + + {error &&

    {error}

    } +
    + {workout.status !== "completed" && ( + + )} + +
    +
    + ); +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..fb12966 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL?: string; + readonly VITE_OFFLINE?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..523bfe9 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} 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..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..e64cf20 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,13 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: 5173, + proxy: { + "/api": "http://localhost:8000", + }, + }, +}); diff --git a/Драфт.xlsx b/Драфт.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9cd6d6dd76b79fcc918979fa17be3d6388834c38 GIT binary patch literal 22896 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 Yq6c`h0++Rc#)d=~yn$mx{2?G70LWAh)c^nh literal 0 HcmV?d00001