OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
Трекер тренировок для тех, кто ходит по разным залам
Translation pending
Sports & fitnessTypeScript · CSSGitHub @ArtsiomKazlouskiOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Трекер тренировок для тех, кто ходит по разным залам. Килограммы на грузоблоке одного производителя не сравнимы с килограммами на другом. Поэтому anygym никогда не пересчитывает вес между железками: история висит на упражнении, а упражнение привязано к одной модели оборудования. Между залами переносится не число, а сама модель — если тот же тренажёр стоит в двух залах, это одна железка и одна история. Плюс приложение помнит, как настроить каждую под тебя, и считает, какой вес поставить прямо сейчас. Проектные решения и алгоритм движка веса: docs/DESIGN.md.Read the full README ↗
Трекер тренировок для тех, кто ходит по разным залам.
Килограммы на грузоблоке одного производителя не сравнимы с килограммами на другом. Поэтому anygym никогда не пересчитывает вес между железками: история висит на упражнении, а упражнение привязано к одной модели оборудования. Между залами переносится не число, а сама модель — если тот же тренажёр стоит в двух залах, это одна железка и одна история. Плюс приложение помнит, как настроить каждую под тебя, и считает, какой вес поставить прямо сейчас.
Проектные решения и алгоритм движка веса: docs/DESIGN.md.
| Слой | Что |
|---|---|
| Приложение | Next.js 16 (App Router), React 19, Tailwind 4 |
| База | Neon Postgres + Drizzle ORM |
| Вход | Auth.js v5, Google, доступ по allowlist |
| Хостинг | Vercel, деплой через Git-интеграцию |
npm install
npm run dev
Нужен заполненный .env.local — образец в .env.example.
DATABASE_URL и DATABASE_URL_UNPOOLED подтягиваются автоматически:
neon link --project-id <id> --branch production
AUTH_GOOGLE_ID и AUTH_GOOGLE_SECRET заводятся вручную в Google Cloud Console
(APIs & Services → Credentials → OAuth client ID → Web application). Redirect URI:
http://localhost:3000/api/auth/callback/google
https://<домен>/api/auth/callback/google
| Команда | Что делает |
|---|---|
npm run dev | Дев-сервер |
npm run build | Продакшн-сборка |
npm run typecheck | Проверка типов без сборки |
npm run lint | ESLint |
npm run test | Тесты движка веса (node:test) |
npm run db:generate | Сгенерировать миграцию из src/db/schema.ts |
npm run db:migrate | Применить миграции |
npm run db:backup | Дамп всех таблиц в backups/*.json |
npm run db:studio | Drizzle Studio |
npm run dry-run | Прогнать движок по плану на реальных данных, ничего не записывая |
Vercel собирает по git push в main — GitHub Actions не нужны, секретов
в GitHub нет.
Vercel → New Project → импортировать ArtsiomKazlouski/anygym.
Next.js определится сам, настройки сборки менять не надо.
Переменные окружения проекта (Settings → Environment Variables), для всех сред:
| Переменная | Кто читает | Откуда взять |
|---|---|---|
DATABASE_URL | приложение | из .env.local — та же база Neon |
AUTH_SECRET | Auth.js | тот же, что локально (см. ниже) |
AUTH_GOOGLE_ID | Auth.js | из .env.local |
AUTH_GOOGLE_SECRET | Auth.js | из .env.local |
ALLOWED_EMAILS | проверка входа | почты через запятую |
Больше ничего не нужно. DATABASE_URL_UNPOOLED читает только
drizzle.config.ts, то есть миграции, а они запускаются локально.
NEON_BRANCH не используется нигде — это метаданные neon link для
собственных нужд CLI. Обе переменные на Vercel мёртвые.
AUTH_SECRET — ключ подписи сессионной куки. Кто им владеет, тот может
выписать себе валидную сессию в обход Google: allowlist проверяется только
в момент входа, а не на каждом запросе.
Разводить прод и локаль разными секретами смысла нет, пока база одна:
DATABASE_URL всё равно общий и даёт прямой доступ ко всем данным. Запирать
форточку при открытой двери. Если понадобится настоящее разделение — завести
отдельную ветку Neon под разработку и переключить на неё .env.local; тогда
и секреты имеет смысл разводить.
ALLOWED_EMAILS обязателен. Пустой список не пускает никого — приложение
при потерянной переменной закрывается, а не открывается.
Google Cloud Console → APIs & Services → Credentials → OAuth client → Authorized redirect URIs, добавить рядом с локальным:
https://<домен>.vercel.app/api/auth/callback/google
Redeploy. Если вход падает на колбэке — добавить AUTH_TRUST_HOST=true.
.env.local смотрит на ту же ветку Neon, что и прод. Значит миграции
применяются локально и попадают на прод сразу:
npm run db:generate # сгенерировать из src/db/schema.ts
npm run db:migrate # применить
Отдельного шага в сборке нет намеренно: билд запускается на каждый деплой, и прогонять DDL оттуда — лишний способ выстрелить себе в ногу.
Локальная разработка идёт на боевой базе. Для приложения на одного человека
это осознанный выбор: одна история тренировок, никакой синхронизации между
средами. Если понадобится разделить — завести отдельную ветку Neon для локали
(neon branch create) и переключить на неё .env.local.
Перед миграцией, которая трогает данные:
npm run db:backup
Дамп всех таблиц в один JSON (backups/, в git не уходит). Не экспорт для
человека, а снимок ровно того, что лежит в базе, в том же виде, в каком его
можно залить обратно.
Со стороны Neon окно мгновенного восстановления на бесплатном тарифе — 6 часов
(history_retention_seconds: 21600). Откатиться «на вчера» без снимка нельзя.
neon snapshots create --name "manual-$(date +%Y%m%d)" # снять копию
neon snapshots list # что есть
neon snapshots restore <id> # восстановить
Снимок на бесплатном тарифе держится один: create на втором отвечает
snapshots limit exceeded. Поэтому рабочая страховка — db:backup, а снимок
Neon имеет смысл обновлять перед крупными изменениями, удалив предыдущий.
Автоматическое расписание не настроено. Включается так:
neon snapshots schedule set --frequency daily --hour 3 --retention 2592000
Приложение отдаёт манифест и иконки, ставится с домашнего экрана как обычное приложение и открывается без адресной строки. iOS: Поделиться → На экран «Домой». Android: меню → Установить приложение.
Вход ограничен списком ALLOWED_EMAILS. Пустой список не пускает никого — это
намеренно: при потерянной переменной окружения приложение закрывается, а не открывается.
src/
app/ маршруты App Router
auth.ts Auth.js с адаптером Drizzle
auth.config.ts edge-безопасная часть конфигурации (её берёт proxy.ts)
proxy.ts проверка входа на каждом запросе (бывший middleware)
db/
schema.ts схема БД — реализует docs/DESIGN.md, раздел 4
index.ts клиент Drizzle поверх Neon
lib/
muscles.ts мышечные группы — список и подписи
engine/ движок веса: сетка железки и назначение веса, чистые функции
session/ сбор истории, план пункта, действия тренировки
equipment/ каталог залов, железа и упражнений
templates/ планы тренировок
components/ разметка экранов
drizzle/ сгенерированные миграции
scripts/ обслуживающие скрипты
backups/ дампы базы (в git не уходят)
docs/DESIGN.md схема данных и движок веса