BikeControl (SwiftControl)
使用 Zwift Click、Zwift Play、Shimano Di2,或任何藍牙控制器搭配 MyWhoosh、Rouvy、Zwift 等。可於 iOS、Android、Windows 與 macOS 免費試用。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Мобильное веб-приложение для ведения силового цикла в фэнтези-сеттинге: герой, уровни, статы, вердикты «отработал / схалявил». Программа — Цикл V «Арена»: сплит Верх / Низ / Фулбоди-добор, 3 тренировки в неделю, 4 нагрузочные недели плюс разгрузочная. Каждая мышечная группа получает два активных дня в неделю по 2–3 упражнения за день, косвенной работы больше. Это адаптация профессиональных бодибилдерских сплитов (Кевин Леврон, Ронни Колеман, Ли Прист) под натурального атлета: специализация дня как у про, но объём на группу в восстанавливаемых рамках, а частота поднята с 1 до 2 раз.檢視完整 README ↗
Мобильное веб-приложение для ведения силового цикла в фэнтези-сеттинге: герой, уровни, статы, вердикты «отработал / схалявил».
Программа — Цикл V «Арена»: сплит Верх / Низ / Фулбоди-добор, 3 тренировки в неделю, 4 нагрузочные недели плюс разгрузочная. Каждая мышечная группа получает два активных дня в неделю по 2–3 упражнения за день, косвенной работы больше. Это адаптация профессиональных бодибилдерских сплитов (Кевин Леврон, Ронни Колеман, Ли Прист) под натурального атлета: специализация дня как у про, но объём на группу в восстанавливаемых рамках, а частота поднята с 1 до 2 раз.
Акценты недели всегда 2 + 1: недели 1 и 3 — Верх и Низ силовые, Фулбоди объёмный; недели 2 и 4 — наоборот. Так каждая группа каждую неделю видит и тяжёлую, и объёмную работу. Недели 3–4 идут на второй волне движений: вспомогательные заменяются родственными вариантами, а движение дня ног — на родственный вариант того же паттерна (присед со штангой → фронтальный присед).
Пятая неделя цикла — разгрузка: те же квесты, те же движения, то же число подходов, но вес −15%
(DELOAD в data/progression.js). Усталость копится быстрее, чем сила, и неделя на 85% возвращает свежесть
до того, как она превратится в застой — это не пропущенная неделя, а условие, при котором следующий блок
стартует сильнее. Такая неделя не двигает рабочий максимум ни вверх, ни вниз: движок помечает её в журнале
(plan.deload) и при чтении истории пропускает. Три разгрузочных квеста подряд не считаются застоем.
Гак-присед раньше попадал почти в каждый второй квест — теперь он остался в двух квестах нагрузочного блока,
а в ротацию добавлены фронтальный присед (движение дня ног на волне B), болгарские выпады и
плие-присед (приводящие в растянутой позиции — им в обычном приседе прямой работы не достаётся).
У каждого свой рабочий максимум: фронтальный стартует от приседа через коэффициент k = 0,82, дальше его
ведут собственные подходы.
У каждого движения в пуле есть цена cns: 3 — максимальная база (присед, становая, фронтальный присед),
2 — тяжёлое многосуставное со свободным весом, 1 — многосуставное на тренажёре, 0,5 — изоляция.
Из неё считается нагрузка квеста (sessionLoad) и действует жёсткое правило: одна максимальная база за сессию.
Присед и становая никогда не попадают в один день, после становой нет отдельной работы на поясницу,
а движения с cns 3 не уходят в многоповторку. Всё это проверяется тестами и не ломается пользовательскими заменами:
если поставить становую рядом с приседом вручную, квест покажет предупреждение о перегрузе.
Становая в цикле одна — отдельный силовой день на третьей неделе, где нет ни приседа, ни наклонов, а квадрицепс добирается тренажёрами. Нагрузка каждого квеста видна в его шапке и в списке квестов.
В квестах живут две сущности: суперсет (два движения подряд без отдыха, задаёт порядок) и дроп-сет (сброс веса на последнем подходе). Движок ставит дроп-сет не чаще одного раза за квест и только на изоляции. Остальные «приёмы интенсивности» из практики про-атлетов убраны: широкой аудитории они дают выбор там, где его не должно быть.
Одно приложение, две подачи — переключается в профиле, блок «Оформление».
| Сага (по умолчанию) | Чистая | |
|---|---|---|
| палитра | золото по чёрному, сине-зелёный туман | чёрный, тёмно-синий, голубой, белый |
| шрифт заголовков | Cormorant (антиква) | Manrope — тот же, что в тексте |
| иконки | game-icons.net (CC BY 3.0) | Lucide (ISC), 24×24, обводка 2px |
| фактуры | кожа, зерно, виньетка, гербовые рамки | нет, плоские поверхности |
| переход между экранами | заставка с руной | без заставки |
| слова | квесты, боссы, саги, баффы, знаки отличия | тренировки, добавки, достижения |
| названия тренировок | «Клинок Титанов» | «Верх тела · силовая» |
| достижения | «Рассветный Клинок» | «Утренняя тренировка» |
| добавки | «Масло Левиафана» | «Омега-3 (рыбий жир)» |
| профиль атлета | уровень, опыт, класс, характеристики | три числа: тренировок, поднято, недель подряд |
Трекер открывают, чтобы записать подход, а не читать. Поэтому во второй редакции
убрано всё, что объясняет очевидное, и ужат воздух. Замерено обходом экранов в браузере
(высота #app при ширине 414 px):
| экран | было | стало |
|---|---|---|
| профиль | 1765 px | 1139 px |
| добавки | 2263 px | 795 px |
| питание | 2207 px | 1587 px |
| прогресс | 1321 px | 771 px |
| тренировки | 677 px | 600 px |
Что именно убрано: уровень, опыт, класс и шкалы характеристик (вместо них три числа, которые атлету действительно нужны); подписи к разделам, дублирующие название вкладки; подсказка «как пользоваться» под каталогом; советы по таймингу питания; вердикт «ПОЧТИ НИЧЕГО» над шкалами; строка «осталось N» под каждой шкалой (её и так видно по полоске); повторяющееся «пока пусто» в каждой из четырёх кривых роста; ежедневные подсказки к добавкам; бейдж «волна A». Каталог добавок занимал 62% экрана — он справочник, и в «Чистой» спрятан под раскрытие. Отступы панелей 18 → 13 px, между панелями 20 → 10 px.
Достижения про уровень и характеристики в «Чистой» не показываются — их раздел скрыт (90 из 100 вместо 100). Сами они никуда не делись: начисляются по-прежнему и видны в «Саге», где этим понятиям есть место.
«Чистая» — не просто другая палитра: у неё своя редакция всех текстов, которые видит
пользователь. Сто достижений переписаны целиком (ACH_PLAIN в data/achievements.js):
те же условия и тот же прогресс, другое название и формулировка. «Триста Спартанцев» →
«300 повторений», «Гора Железа» → «8 тонн за тренировку», «Перекованный» → «+35% к
стартовым весам». Разделы списка тоже свои: «Путь героя» → «Путь», «Дисциплина» →
«Регулярность», «Герой» → «Форма», «Судьба» → «Бонусы».
Названия добавок в «Чистой» берутся из поля real, которое и так лежит в данных:
в каталоге стоит «Креатин моногидрат», а не «Эликсир Силы». Класс персонажа становится
профилем подготовки, который считается из тех же характеристик. Заметки в журнале
получений («тренировок: 42» вместо «квестов: 42») пишутся в редакции, которая была
включена в момент получения, и задним числом не переписываются — это история.
Переключение темы ничего не обнуляет: прогресс, достижения и журнал общие.
Тема — это три слоя, и живут они в data/theme.js: палитра и типографика
(css/theme-plain.css по атрибуту data-theme), набор иконок и словарь.
Остальное приложение про темы не знает.
css/styles.css остаётся темой «Сага» без изменений: все фэнтезийные цвета в нём
вынесены в токены-тройки (--t-acc, --t-sec, --t-bad, --t-good и т.п.), поэтому
вторая тема — это переопределение токенов плюс полсотни правил там, где фэнтезийность
зашита в сам рисунок: градиенты, тиснение, фактуры, геральдика. Все правила второй темы
заперты внутри :root[data-theme="plain"], и это проверяется тестом — иначе они утекут
в «Сагу». Перевод литералов в токены был сверен покомпонентно: 34 749 вычисленных
свойств по всем экранам, ноль расхождений.
Тема ставится до первой отрисовки крошечным скриптом в index.html: он читает
отдельный ключ localStorage, иначе при каждом запуске на долю секунды мигала бы не та
тема. Источник истины при этом — настройки журнала, скрипт лишь снимает мигание.
Выбран Lucide: ISC (можно коммерчески, без указания авторства в интерфейсе), 2100+ иконок на единой сетке 24×24 с обводкой 2px, один стиль без вариантов — то, что нужно, когда интерфейс должен выглядеть собранным. Рассматривались Phosphor (MIT, 9000+ иконок в шести начертаниях — избыточно), Tabler (MIT, 5900+, очень близкий аналог) и Heroicons (MIT, но всего ~300 иконок — не хватило бы на достижения).
Иконки не тянутся с CDN: tools/build-plain-icons.mjs вынимает нужные 118 из пакета
lucide-static и кладёт в data/icons-plain.js (27 КБ) — ровно тем же способом, каким
в проекте уже собраны остальные наборы. Семьдесят пять фэнтезийных медалей достижений
сведены к полутора десяткам предметных значков (кубок, медаль, мишень, календарь).
Анатомические иконки движений (data/icons-exercise.js) в обеих темах одни и те же:
они и так предметные, и это самая полезная графика в приложении.
Почти чёрный фон, тёмно-синие панели, ОДИН голубой акцент на всё приложение и белый текст — так сегодня выглядят и трекеры силовой (Hevy, Strong, Boostcamp), и продуктовые интерфейсы вроде Linear и Vercel. Общее у них одно: цвет работает указателем, а не украшением. Поэтому в «Чистой» голубым подсвечено только то, что сейчас важно — рабочий вес, активная вкладка, следующая тренировка; всё остальное живёт на оттенках серо-синего.
api/ + Postgres (Neon). Настройка — в SETUP.md, раздел «Сервер журнала»data/program.jsdata/exercises.jsdata/achievements.js; иконки достижений — data/icons-achievements.jsdata/theme.js (темы + словарь), css/theme-plain.css, data/icons-plain.jsКлиент (js/):
model/ — журнал, расчёты, синхронизация. Не знает про DOM и экраны; о событиях сообщает
подписками (onSaved, onAchievements, состояние сервера и облака);view/ — разметка: получает готовые данные, ничего не меняет и не вешает обработчиков;controller/ — экраны: берут данные из модели, отдают виду, обрабатывают действия;main.js — единственная точка входа, связывает модель с интерфейсом.Сервер (server/, маршруты — тонкие файлы в api/):
models/ — SQL-запросы, только с параметрами; views/ — что уходит клиенту;
controllers/ — порядок проверок и работа с журналом; security/ — подпись Telegram,
проверка тела, источник запроса; db/ — подключение, роль приложения, миграции.Границы слоёв закреплены тестом tests/architecture.test.mjs (клиент) и tests/server.test.mjs (сервер).
Панели «Сохранение» (экспорт/импорт JSON и ручные кнопки облака) и «Приватность» убраны с экрана: первая вернётся, когда доработаем резервные копии, вторая была справкой для разработчика. Код экспорта и импорта на месте, достижения «Хранитель Хроник» и «Возрождённые Хроники» ждут его возвращения. Перенос журнала текстовым кодом вырезан совсем — внутри Телеграма журнал и так уезжает в облако аккаунта.
npx serve .
# или python3 -m http.server 8000
С сервером журнала — на своём Postgres, тем же кодом, что на Vercel:
npm ci
DATABASE_URL=postgres://… DATABASE_DRIVER=pg BOT_TOKEN=… node tools/dev-server.mjs # ALLOWED_USERS=… — закрытая бета
node --test tests/*.mjs # без базы
TEST_PG_ADMIN_URL=postgres://postgres:…@localhost:5432/postgres npm run test:db # на живом Postgres
tests/server.test.mjs — сервер без базы: подпись Telegram (подмена, чужой токен, просрочка, дубли полей),
открытая регистрация и закрытая бета, проверка тела (загрязнение прототипа, NUL, битый Unicode, глубина, размеры), источник
запроса, и статические запреты: ни одного SQL из склейки строк, process.env только в настройках,
в журнал функций не пишутся ни данные, ни токены.tests/db/server.test.mjs — сервер на живом Postgres (в CI — отдельная задача с Postgres 16, в двух
режимах владельца базы): миграции и одновременный старт, права роли приложения, RLS (чужой журнал не
прочитать, не изменить и не подделать даже прямым SQL), инъекции, гонки записей, история версий,
атаки на API (подделка подписи, чужой сайт, неверные методы и типы, 3 МБ+ с лживым Content-Length,
не-UTF-8), лимиты частоты, жёсткая граница отдельного логина.tests/foods.test.mjs и tests/foods-model.test.mjs — продукт «на 100 г» (с порции, из кДж, по БЖУ),
клетчатка и вода, напитки по названию без ложных срабатываний, свои продукты, обычная порция, приёмы
пищи, бережный поиск в Open Food Facts (от 3 букв, по паузе, 8 в минуту, запоминание ответов).tests/db/foods.test.mjs — каталог на живом Postgres: поиск с опечатками, пополнение только с
перепроверкой в OFF по штрихкоду, спецсимволы LIKE и SQL в запросе, лимиты.tests/e2e/foods.mjs — вкладка питания в браузере: каталог, OFF по паузе, предохранитель,
экранирование названий, приём пищи, журнал с записью на сервере.tests/sync-server.test.mjs — решения синхронизации: перенос, новое устройство, конфликт, сравнение
журналов без учёта порядка ключей.tests/e2e/server-sync.mjs — сквозной прогон в браузере (не в CI): перенос журнала, второе устройство,
правка, конфликт, работа без сети, новый человек (сам заводится в базе; со списком — не пускается),
обычный браузер без Telegram.tests/program.test.mjs — периодизация (2+1 в неделю), разгрузочная неделя (один состав и то же число
подходов, что в нагрузочной), активная частота 2×/нед на группу, объём за неделю и за сессию, концентрация
по группам внутри сессии и накопленная работа к каждому движению, волны разнообразия, дозировка приёмов
интенсивности, парность суперсетов, сборка квеста, правки состава, расчёт рабочих весов, недельный тоннаж
и тренд, целостность пула и архива.tests/achievements.test.mjs — достижения: пороги, счётчики повторяемых, журнал получений, миграция.tests/theme.test.mjs — две темы: обе описаны полностью, в словаре ни один ключ не потерян
и нет лишних, у каждой реально отрисовываемой иконки есть штриховая пара, фрагменты SVG
валидны, стили второй темы не протекают в «Сагу». Отдельно — словарь запрещённых слов:
ни в одной строке «Чистой» и ни в одном из ста достижений не должно быть фэнтези.
Сравнение идёт по началу слова, иначе «превратится» ловится на «врат», а «отмеченных» —
на «меч». Там же: все сто достижений переписаны, лишних записей нет, названия не
повторяются, а исходная редакция не изменилась. Плюс: в «Чистой» скрыт ровно раздел
про уровень и характеристики, сами достижения из данных не пропали, а счётчик считает
по видимым.У цифровой клавиатуры iOS нет кнопки «готово»: закрыть её можно только тапом мимо поля. Поэтому пока поле в фокусе, над клавиатурой висит своя полоска «Готово», а тап по пустому месту снимает фокус сам. Полоска прячется не мгновенно: iOS дошлёт по тем же координатам клик, и если полоски уже нет, он прилетит по тому, что под ней.
Пока клавиатура открыта, нижние панели уезжают с экрана (body.kbd): и таб-бар, и плашка отдыха прибиты
к низу окна, а клавиатура этот «низ» сдвигает — на iOS они начинают болтаться посреди экрана. Заодно на
это время замирают отступы безопасных зон Телеграма: он шлёт viewportChanged с другими значениями, и
вёрстка прыгала прямо под пальцем. После закрытия клавиатуры всё возвращается, плюс короткий толчок
скролла — iOS иногда оставляет fixed-элементы там, где был сдвинутый вьюпорт.
Два места, где клавиатура раньше ломалась, и почему:
touchend и звала preventDefault() на любое второе касание
за 350 мс — быстрый тап просто не доходил до страницы, поле не теряло фокус, и клавиатуру нельзя было
ни закрыть, ни открыть заново. Двойной тап и так не зумит: за это отвечает touch-action: manipulation;Принцип — функциональная простота: на экране бейджи, объяснения под кнопкой «?».
квадрицепс 10): на какую группу приходится основной объём сессии.
Он же краснеет, если правка атлета пробила потолок в 10 подходов на группу. В раскрытой карточке движения
подписано, почему вес ниже ожидаемого: «квадрицепс к этому моменту уже отработал 7 подходов — вес на 10%
ниже, чем на свежую мышцу».58,8 т), сколько это рабочих подходов и процент к прошлой непустой неделе (▲ +11% к неделе 1).
Это единственное число, в котором «я себя перегружаю» видно раньше, чем это почувствуют колени: план
показывает вес в подходе, а не сумму за неделю. Разминка не в счёт, повторный заход в тот же квест
заменяет прошлый — считается неделя, а не количество попыток (weekTonnage / tonnageTrend).102,5 кг × 4–6 повт), счётчик закрытых подходов и точки.
Раскрытая начинается с блока цели: слева крупно вес и повторы, справа мелко пол движения и куда поехал
вес (▲ +2,5 кг), ниже свёрнутая строка «прошлый раз» (раскрывается по тапу: в зале она нужна не каждый
подход). Заголовка у блока нет — и так понятно, что делать. Ярлыки RPE/RIR с экрана убраны: широкой
аудитории они ничего не говорят, а движку прогрессии запас по-прежнему нужен и живёт в данных.
Закрытое движение гаснет, чтобы взгляд цеплялся за незакрытые.data/exercises.js — справочник движений: мышечная группа, паттерн, снаряд, уровень (для таймера отдыха),
описание, техника и коэффициент k для оценки 1ПМ от базового лифта.
Вес в подходах не зашит в программу и не берётся из рекордов — его ведёт журнал (data/progression.js).
Правило одно, двойная прогрессия по каждому движению:
Разминочные подходы (легче 80% рабочего веса) движок отсекает сам — и в разборе сессии, и в счёте квеста,
и в карточке движения. Правило одно, isWarmup() в data/progression.js.
Всё раскрывается из одного числа — рабочего максимума движения. Из него считается рабочий вес на сегодня: сколько повесить в этой схеме. Его квест и подставляет в подходы, поэтому в зале считать нечего. Рядом идёт пол — 90% рабочего веса: ниже него подход перестаёт быть рабочим. Это та же граница, за которую движок не пускает рабочий максимум при серии неудач, и по ней же считается интенсивность квеста.
Схемы цикла чередуются (силовая 4–6, объёмная 8–10, добивающая 15–20), поэтому рабочий максимум хранится в пересчёте на максимум по одной кривой «проценты от 1ПМ» со скидкой на запас (RIR). Прибавка, взятая на объёмной неделе, не теряется на силовой, а подход на 20 повторов не получает вес десятиповторного.
Объём и отдых входят в расчёт. Один подход на десять повторов и четыре таких подхода по полторы минуты
отдыха — это разные веса, хотя повторы в плане одни и те же: к последнему подходу копится усталость.
Поправка (loadFactor): каждый подход после первого стоит 2%, каждые полминуты недоотдыха до 2,5 минут —
ещё 1,5%, глубже 18% не идём. Работает в обе стороны: когда вес назначается — вниз, когда журнал читается —
вверх, иначе один и тот же квест занижал бы сам себя с каждым разом. Отдых берётся из общего справочника
(restFor): тяжёлой базе от четырёх минут, многосуставному от двух, изоляции минута-полторы — те же числа
показывает таймер отдыха, чтобы план и расчёт не разъезжались.
Из-за этого одно движение получает разный вес в разных квестах: жим с рабочими 102,5 кг на силовой неделе (4×4–6) на объёмной (4×8–10) даёт 90 кг, а не те же 102,5.
Накопленная за тренировку работа входит в расчёт. loadFactor считает усталость внутри упражнения,
а между упражнениями её считает freshFactor: разгибания ног третьим движением идут не на свежий квадрицепс —
к ним он уже отработал семь подходов приседа и жима ногами. Каждый сделанный до движения рабочий подход по той
же группе снимает 1,5%, вторичная работа (ягодицы в приседе, трицепс в жиме) считается вполовину, глубже 15%
не идём. Движение дня всегда первое и всегда получает вес как на свежую мышцу.
Поправка симметрична: при назначении веса делит, при чтении журнала умножает обратно, а накопленную работу движок восстанавливает из снимка состава сессии — поэтому и сессии, записанные до появления поправки, читаются по тем же правилам, и при неизменном составе квеста вес не меняется. Двигается он ровно тогда, когда меняется состав или порядок: уберёшь жим ногами — разгибания подрастут с 35 до 40 кг, добавишь ещё одно движение на квадрицепс — соседи просядут. Это же правило закрывает старую дыру: первый заход в подсобку раньше считался от свежего максимума (жим ногами третьим в квесте получал 185 кг вместо 170).
В шапке квеста стоит бейдж концентрации (квадрицепс 10) — на какую группу приходится основной объём сессии.
Потолок SESSION_CAP = 10 подходов на группу за сессию: выше добавочный подход уже не растит, а только
отнимает восстановление (Baz-Valle 2022). Шаблоны в потолок укладываются, а если его пробивает правка атлета,
бейдж краснеет и в разборе квеста написано, что убрать. Полная раскладка по группам — там же, под «?».
Самочувствие входит в расчёт. Перед квестом атлет отмечает состояние — это единственное, чего движок не может вывести из журнала: сон, стресс и болезнь в подходы не записываются.
| состояние | вес | подходы | отдых |
|---|---|---|---|
| Свежий | +2% | как в плане | ×0,85 |
| Норма | план как есть | как в плане | как в плане |
| Устал | −5% | на один меньше (не ниже двух) | ×1,25 |
Поправка симметрична, как и поправка на объём: движок помнит, в каком состоянии закрыт квест (sessions[].feel),
и при чтении истории делит её обратно — тяжёлый день не читается как откат силовых. Состояние живёт
в журнале и сбрасывается на «норму» на следующий день: вчерашняя разбитость не должна молча резать сегодняшний
квест. На том же жиме 102,5 кг силовая схема даёт 105 кг свежему и 100 кг разбитому — при трёх подходах вместо четырёх.
Личный максимум (расчётный 1ПМ, среднее формул Эпли и Бжицки с капом на 10 повторах) живёт отдельным счётчиком в карточке движения и в «Хрониках». В подборе веса он не участвует: рекорд одного удачного дня не должен задирать рабочую неделю. Рекордный сингл в конце квеста идёт в этот счётчик, но рабочий вес не роняет.
Вес героя меняется в профиле по тапу: от него считаются подтягивания, брусья и гиперэкстензия — там рабочий вес это довесок к своему, и устаревшее число врёт в каждом подходе.
Поправить рабочий вес руками можно в карточке движения: кнопки −10% / −5% / +5% и «↺ расчёт».
Правка запоминается датой (S.workReset), и с этого дня отсчёт начинается заново: всё, что было раньше,
в расчёт не идёт, а дальше вес снова ведут подходы. Это для возвращения после болезни или перерыва —
догонять шагами по 2,5 кг оттуда, где ты уже не стоишь, значит потерять месяц.
Пока по движению нет подходов, вес стартует от базовых лифтов через коэффициент k (◎) с плановой
надбавкой недели; как только появляются подходы, вес двигают они (★), а надбавка из шаблона отключается.
Для гантелей вес делится на руку, для подтягиваний и брусьев выдаётся довеском к своему весу — считается
всё по полной системе, а показывается то, что реально вешаешь.
«Хроники прокачки» показывают по каждому движению рабочий вес на следующий раз, личный максимум, тренд в %/месяц и состояние: рост, держим, застой (3 квеста без прибавки — делоад или смена движения) или откат.
В любом квесте движение можно заменить (подбираются похожие по паттерну и группе), добавить из арсенала
или убрать — правки хранятся в S.plan[wid] и не трогают шаблон. Схема подходов наследуется от роли слота,
поэтому замена движения дня остаётся движением дня. Подстраница «Арсенал движений» в разделе квестов
показывает весь пул с рабочими весами атлета, разбор техники и недельное покрытие мышц.
Ключевые решения опираются на мета-анализы и RCT 2020–2026:
100 уникальных достижений в шести рангах: бронза → серебро → золото → алмаз → изумруд → титан.
Уникальные (вехи, клубы силы, серии) выдаются один раз; повторяемые (рекорд, безупречный квест, тоннаж за квест,
дары судьбы) при каждом новом получении увеличивают счётчик ×N. У каждого знака есть журнал получений:
дата и причина (тоннаж и квест, рекорд с килограммами, время, серия) — виден в раскрытии знака. Правила — чистые функции над контекстом
(buildAchievementCtx в js/app.js), список и оценка — data/achievements.js.
Иконки — game-icons.net (CC BY 3.0); новые вытаскиваются скриптом tools/extract-icons.py из локального клона библиотеки.
data/icons-plain.js) — Lucide, ISC
(© Lucide Contributors; набор вырос из Feather, © 2013–2023 Cole Bemis).
Файл собирается скриптом tools/build-plain-icons.mjs из пакета lucide-static — там же
таблица «какое имя в приложении какой иконке Lucide соответствует». Руками файл не правится.
Текст лицензии — data/icons-plain-LICENSE.txt.data/bodymap.js собирается скриптом tools/build-bodymap.mjs: он берёт модель, раскачивает её
до телосложения атлета (ширина по профилю высоты, руки переносятся отдельно) и раскладывает части
модели по нашим группам мышц. Руками файл не правится. Текст лицензии — data/bodymap-LICENSE.txt.
Служебные значки снарядов и приёмов (data/icons-ui.js) нарисованы для проекта.data/icons-exercise.js) — из двух открытых наборов:
game-icons.net (CC BY 3.0; авторы Lorc, Delapouite, sbed и др.) и
healthicons.org (MIT). Файл собирается скриптом
tools/build-exercise-icons.mjs — там же таблица «какой значок какому движению» и правило раскладки:
многосуставное движение узнаётся по самому движению, изоляция — по мышце, которую грузит.
Руками файл не правится. Текст лицензий — data/icons-LICENSE.txt.Приложение работает и как обычная веб-страница, и как мини-приложение Telegram: внутри Телеграма подхватываются системная кнопка «Назад», тактильная отдача, безопасные зоны, цвет шапки и облако.
Что делает код (js/telegram.js):
ready() и expand() на старте, блокировка свайпа вниз, иначе жест закрывает приложение прямо во время подхода;HapticFeedback, потому что navigator.vibrate в Телеграме на iOS не работает;safeAreaInset и contentSafeAreaInset пробрасываются в CSS-переменные --tg-top и --tg-bottom;CloudStorage уходит резервная копия:
состояние режется на части по 3800 символов, мета-запись пишется последней, хвост прошлого
сохранения подчищается. 22 КБ журнала это 7 частей.Вне Телеграма ничего из этого не включается: панели облака нет, навигация работает своими кнопками.
Если сервер настроен, главная копия журнала — в Postgres. Регистрация открытая: при первом запуске
человек заводится в базе сам (с ALLOWED_USERS — только те, кто в списке):
initData, HMAC токеном бота); id из тела
запроса на веру не берётся;migrate;rev журнала и serverRev, от которой он считан. Новое устройство забирает журнал
молча, правки с двух устройств — вопрос, какую версию оставить, вторая остаётся резервной копией;SETUP.md;Защита на сервере:
bu_app без права обходить RLS, на таблицах FORCE ROW LEVEL SECURITY: база сама
отдаёт каждому только его строки, даже при ошибке в коде. С DATABASE_URL_APP — отдельный логин,
и граница жёсткая даже для произвольного SQL;no-store, ошибки — только код, без подробностей;server/, node_modules/, package*.json снаружи не отдаются; тесты и утилиты на Vercel не уезжают.Без сервера приложение тоже мультипользовательское:
bodyupgrade.v1.u<id>), поэтому на одном
телефоне могут заходить несколько аккаунтов и прогресс не смешивается;CloudStorage изолирован связкой «бот + пользователь», так что облако у каждого своё;rev, у синхронизации — syncedRev. Направление выбирает чистая функция
decideSync: заливать, тянуть, ничего не делать или спросить пользователя, если журнал менялся
и на устройстве, и в облаке;Открыл приложение с другого телефона под тем же аккаунтом — журнал подтянется сам.
/newbot, задать имя и username, получить токен./newapp → выбрать бота → название, короткое описание, иконка 640×360, ссылка на продакшн (адрес Vercel) →
короткое имя приложения. Появится прямая ссылка вида t.me/<bot>/<app>./mybots → бот → Bot Settings → Menu Button → задать URL приложения, чтобы кнопка меню в чате открывала его./setdescription, /setabouttext, /setuserpic.Шаги 3 и 4 можно не делать руками — есть скрипт, токен в репозиторий не попадает:
BOT_TOKEN=123456:AA... APP_URL=https://твой-проект.vercel.app ./tools/setup-bot.sh
Он ставит кнопку меню на мини-приложение, имя, описания и команды бота. Регистрацию самого
мини-приложения (/newapp) Bot API не умеет, её проходят в BotFather один раз.
Проверять удобно в Telegram Desktop: правый клик по мини-приложению → «Inspect» открывает devtools.
Ничего секретного в репозитории нет и быть не должно.
.env закрыт в .gitignore, в репозитории лежит только .env.example с пустыми значениями;.env, а токен вырезает из любого вывода, чтобы он не попал в логи;.githooks/pre-commit отменяет коммит, если в изменениях виден токен или файл .env.
Включается один раз: git config core.hooksPath .githooks;./tools/check-secrets.sh --history проверяет рабочее дерево и историю коммитов;tests/security.test.mjs.Если токен всё же где-то засветился — сразу /revoke в BotFather, старый перестанет работать.
Журнал лежит на устройстве, в облаке Telegram (доступно только самому пользователю) и — для на сервере приложения, в базе, где каждая строка закрыта политикой RLS.
| Адрес | Зачем | Что уходит |
|---|---|---|
свой /api/journal | сервер журнала | журнал и подпись Telegram |
свой /api/foods | общий каталог продуктов | поисковая строка; штрихкод выбранного продукта Open Food Facts |
| telegram.org | SDK мини-приложения | ничего, обычная загрузка скрипта |
| world.openfoodfacts.org | поиск продуктов | только введённое слово, и только если поиск включён |
Больше приложение никуда не ходит:
assets/fonts, поэтому Google не видит пользователей (обновить: tools/fetch-fonts.sh);initData уходит только на свой /api/
(без cookies и перенаправлений) — это закреплено тестом;vercel.json задана политика безопасности: браузер заблокирует обращение к любому адресу,
которого нет в списке, а также запрещены камера, микрофон, геолокация и передача реферера.Вкладка устроена как в популярных дневниках питания:
Откуда продукты, от быстрого к медленному:
/api/foods): продукты Open Food Facts, которые кто-то уже ел.
Пополняется сам: выбрали продукт OFF — сервер сам берёт его из OFF по штрихкоду, считает на
100 г и проверяет. Значениям с телефона сервер не верит, иначе любой мог бы испортить каталог;Каждый продукт сводится к одному формату (per100 в data/nutrition.js): значения на 100 г (мл),
клетчатка, признак напитка и индекс гидратации — от них считаются шкала клетчатки и вода.
Данные Open Food Facts — по лицензии ODbL, подпись в выдаче.
Пуш в main → Vercel собирает автоматически (Framework Preset: Other, без build command, output = корень).
Функции api/ — Node 22, регион fra1 (рядом с базой). Секреты — только в переменных окружения Vercel.
Счёт = 65% покрытие плановых подходов + 35% выход на свой рабочий вес (движение дня весит ×2).
XP → уровень героя. Статы: СИЛА (рост расчётных 1ПМ от базы 147/170/195/100), ВЫНОСЛИВОСТЬ (средний тоннаж), ДИСЦИПЛИНА (частота за 14 дней), СТОЙКОСТЬ (средний счёт).
tests/progression.test.mjs — прогрессия рабочих весов: шаг вверх и вниз, застой, лёгкий день, гантели
на руку и довесок к своему весу, перенос прибавки между силовой и объёмной схемой, личный максимум отдельно,
поправки на объём/отдых, самочувствие и накопленную за тренировку работу (все в обе стороны), разгрузка
не двигает максимум и не считается застоем, тоннаж без разминки.plan.pair / plan.unpair) и не трогают шаблон. Пересчёт идёт сам:
в паре на разные группы мышца отдыхает, пока работает партнёр, — отдыха даже больше обычного; пара на
одну группу это двойной подход без отдыха, и рабочий вес под неё падает. Об этом сказано и в списке
кандидатов, и в шапке собранной пары.