forked from admin/runners-calendar
Compare commits
5 Commits
fix/races-
...
docs/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 564c223f77 | |||
| 4c77efe76a | |||
| 4f61d9a106 | |||
| d2c54614af | |||
| 36c2042da5 |
95
CHANGELOG.md
95
CHANGELOG.md
@@ -11,9 +11,34 @@
|
||||
|
||||
#### Added
|
||||
|
||||
- Управление паролем и сессиями: проверка текущего пароля, список активных сессий, отзыв выбранной или всех остальных сессий.
|
||||
- Фильтрация стартов по статусу, периоду и дистанции; backfill и обновление автоматически получаемых обложек.
|
||||
|
||||
### [1.5.3] — 2026-07-12
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Миграции базы данных автоматически выполняются перед запуском backend-контейнера.
|
||||
|
||||
### [1.5.2] — 2026-07-12
|
||||
|
||||
#### Added
|
||||
|
||||
- Настраиваемое имя публичного профиля и раздельная видимость будущих и завершённых стартов.
|
||||
- Публичные результаты финиша с учётом настроек видимости владельца.
|
||||
|
||||
### [1.5.1] — 2026-07-12
|
||||
|
||||
#### Added
|
||||
|
||||
- Публичные страницы пользователей и API опубликованного календаря стартов.
|
||||
|
||||
### [1.5.0] — 2026-07-12
|
||||
|
||||
#### Added
|
||||
|
||||
- Проверка и смена текущего пароля.
|
||||
- Список активных сессий, отзыв выбранной сессии или всех остальных сессий.
|
||||
|
||||
### [1.4.2] — 2026-07-12
|
||||
|
||||
#### Changed
|
||||
@@ -83,6 +108,12 @@
|
||||
|
||||
## Frontend
|
||||
|
||||
### [0.10.2] — 2026-08-14
|
||||
|
||||
#### Fixed
|
||||
|
||||
- По умолчанию завершённые старты отображаются от самых свежих к более старым; явно выбранная сортировка применяется без переопределения.
|
||||
|
||||
### [0.10.1] — 2026-08-13
|
||||
|
||||
#### Changed
|
||||
@@ -101,17 +132,48 @@
|
||||
|
||||
- В конфигурацию TypeScript для Vite добавлены DOM-типы.
|
||||
|
||||
### [0.9.3] — 2026-07-14
|
||||
### [0.9.3] — 2026-07-13
|
||||
|
||||
#### Added
|
||||
#### Changed
|
||||
|
||||
- Экран «Аккаунт»: подтверждённый email, смена пароля и управление другими активными сессиями.
|
||||
- На дашборде появились фильтруемые «Последние старты» с пагинацией и переходом в старт, ссылки из карточек рекордов и интерактивный график прогресса.
|
||||
- Поиск, фильтры, сортировка и экспорт истории; график прогресса переведён на AG Charts.
|
||||
- Фильтры календаря сворачиваются и показывают число активных условий; hero-блок дашборда стал компактнее.
|
||||
|
||||
### [0.9.1] — 2026-07-13
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Упрощён обзор календаря стартов и зарегистрированы модули AG Charts.
|
||||
- Зарегистрированы необходимые модули AG Charts для корректного отображения графика.
|
||||
|
||||
### [0.9.0] — 2026-07-13
|
||||
|
||||
#### Added
|
||||
|
||||
- Поиск, фильтры и сортировка истории стартов с синхронизацией параметров в URL.
|
||||
- Экспорт выбранных стартов в CSV и iCalendar.
|
||||
- Интерактивный график прогресса на AG Charts.
|
||||
- Фильтрация стартов по статусу, диапазону дат и дистанции.
|
||||
|
||||
### [0.8.2] — 2026-07-12
|
||||
|
||||
#### Added
|
||||
|
||||
- Имена публичных профилей и отдельные настройки видимости будущих и завершённых стартов.
|
||||
- Отображение опубликованных результатов на публичной странице.
|
||||
|
||||
### [0.8.1] — 2026-07-12
|
||||
|
||||
#### Added
|
||||
|
||||
- Публичная страница пользователя с опубликованным календарём стартов.
|
||||
- Управление видимостью публичного профиля в аккаунте.
|
||||
|
||||
### [0.8.0] — 2026-07-12
|
||||
|
||||
#### Added
|
||||
|
||||
- Экран «Аккаунт»: подтверждённый email, смена пароля и управление активными сессиями.
|
||||
- Фильтруемые «Последние старты» с пагинацией и переходом в старт на дашборде.
|
||||
- Ссылки из карточек рекордов и интерактивный график прогресса.
|
||||
|
||||
### [0.7.1] — 2026-07-12
|
||||
|
||||
@@ -165,6 +227,25 @@
|
||||
|
||||
- Редизайн дашборда и визуального представления стартов.
|
||||
|
||||
### [0.4.3] — 2026-04-13
|
||||
|
||||
#### Fixed
|
||||
|
||||
- В календаре скрыты всплывающие карточки для дней без стартов.
|
||||
|
||||
### [0.4.2] — 2026-04-13
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Анимация наведения применяется ко всей строке старта, а не только к внутренней ссылке.
|
||||
|
||||
### [0.4.1] — 2026-04-13
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Старт автоматически получает статус «Пробежал» при вводе корректного времени финиша.
|
||||
- Исправлены переходы с дашборда и поведение списка и календаря стартов.
|
||||
|
||||
### [0.4.0] — 2026-04-13
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Backlog Calendar Run
|
||||
|
||||
**Срез репозитория: 12 июля 2026.** Это единый продуктовый и технический backlog, составленный по текущему коду, а не список абстрактных идей.
|
||||
**Срез репозитория: 14 августа 2026.** Это единый продуктовый и технический backlog, составленный по текущему коду, а не список абстрактных идей.
|
||||
|
||||
## Текущая база
|
||||
|
||||
| Контур | Версия | Уже работает |
|
||||
|---|---:|---|
|
||||
| Backend | `1.6.0` | Express + PostgreSQL, миграции и seed, строгий CRUD личных стартов, фильтры истории, cookie-сессии, CSRF, подтверждение email, Turnstile, SMTP, смена пароля и управление сессиями, SSRF-защита и восстановление обложек, `/health`, `/ready`, `/meta`, CI с PostgreSQL. |
|
||||
| Frontend | `0.9.0` | Защищённые маршруты и auth-экраны, экран аккаунта, поиск/фильтры/экспорт истории, нативные поля даты/старта, удобный ввод результатов, дашборд с последними стартами и графиком AG Charts, версия FE/BE в футере. |
|
||||
| Frontend | `0.10.2` | Защищённые маршруты и auth-экраны, экран аккаунта, поиск/фильтры/экспорт истории, нативные поля даты/старта, удобный ввод результатов, дашборд с последними стартами и графиком AG Charts, версия FE/BE в футере. |
|
||||
|
||||
## Правила версионирования и выпуска
|
||||
|
||||
@@ -27,6 +27,19 @@
|
||||
|
||||
`2.0.0` не планируется заранее: его поводом может быть только подтверждённая потребность в совместном доступе, командах или публичных стартах — это изменит модель владельцев и доступа.
|
||||
|
||||
## Ближайшие изменения — CI/CD
|
||||
|
||||
| ID | Приоритет | Задача | Результат / критерий приёмки |
|
||||
|---|---|---|---|
|
||||
| OPS-CICD-01 | P0 | Стабилизировать Gitea Runner | Workflow не остаётся в `queued`: runner автоматически забирает job `ubuntu-latest`, изолированно выполняет текущий backend/frontend CI и возвращает итоговый статус в PR. |
|
||||
| OPS-CICD-02 | P0 | Закрыть пробелы текущего CI | Добавлены frontend-регрессионный тест сортировки стартов, полный TypeScript type-check, `git diff --check`, проверка согласованности package/lock версий и сверка CHANGELOG с SemVer-историей. |
|
||||
| OPS-CICD-03 | P0 | Проверять production-образы | CI собирает оба Dockerfile и запускает минимальный smoke-test стека: frontend отвечает `200`, `/api/health` возвращает актуальную backend-версию, `/api/ready` подтверждает PostgreSQL. |
|
||||
| OPS-CICD-04 | P0 | Защитить `main` | Merge разрешён только для актуальной ветки без конфликтов после успешного обязательного `build-and-test`; прямой push в `main` запрещён. |
|
||||
| OPS-CICD-05 | P1 | Публиковать immutable-образы | После зелёного CI в Gitea Container Registry публикуются frontend/backend-образы с тегом commit SHA; SemVer-тег добавляется при релизе, `latest` не используется как единственный источник версии. |
|
||||
| OPS-CICD-06 | P1 | Добавить ручной production deploy | После зелёного CI отдельный manual workflow по SSH обновляет Synology на выбранный SHA, использует Gitea Secrets, не перезаписывает `.env`/данные, выполняет Compose update и итоговые health/readiness-проверки. |
|
||||
| OPS-CICD-07 | P1 | Обеспечить rollback | Перед deploy фиксируется предыдущий SHA образов; при неуспешном smoke-test стек возвращается на него. Процедура проверена без восстановления базы, а миграции до запуска классифицированы как обратимые либо требуют backup. |
|
||||
| OPS-CICD-08 | P2 | Добавить эксплуатационные проверки | По расписанию выполняются dependency/security scan и очистка старых образов; ошибки CI/CD отправляются владельцу. Backup/restore остаются частью `BE-170-01`, а не маскируются успешным deploy. |
|
||||
|
||||
## P0 — BE 1.4.2 / FE 0.7.1
|
||||
|
||||
### Backend 1.4.2
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "calendar-run-frontend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "calendar-run-frontend",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"dependencies": {
|
||||
"ag-charts-community": "^14.0.0",
|
||||
"ag-charts-react": "^14.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "calendar-run-frontend",
|
||||
"private": true,
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getRaceStatusClassName,
|
||||
getRaceStatusLabel,
|
||||
parseRaceDate,
|
||||
sortByDateAsc,
|
||||
sortByDateDesc,
|
||||
downloadTextFile,
|
||||
racesToCsv,
|
||||
racesToIcs,
|
||||
@@ -37,6 +37,7 @@ const VIEW_STORAGE_KEY = "races-view-mode";
|
||||
type ViewMode = "list" | "calendar";
|
||||
type RaceListTab = "upcoming" | "completed";
|
||||
type RaceSort = "dateAsc" | "dateDesc" | "titleAsc" | "distanceAsc";
|
||||
type RaceSortOption = RaceSort | "";
|
||||
|
||||
function yearSelectOptions(races: Race[], selectedYear = ""): number[] {
|
||||
const current = new Date().getFullYear();
|
||||
@@ -146,7 +147,7 @@ export function RacesPage(): JSX.Element {
|
||||
const [distanceMin, setDistanceMin] = useState<string>(() => searchParams.get("distanceMin") ?? "");
|
||||
const [distanceMax, setDistanceMax] = useState<string>(() => searchParams.get("distanceMax") ?? "");
|
||||
const [searchQuery, setSearchQuery] = useState<string>(() => searchParams.get("q") ?? "");
|
||||
const [sortOrder, setSortOrder] = useState<RaceSort>(() => (searchParams.get("sort") as RaceSort | null) ?? "dateAsc");
|
||||
const [sortOrder, setSortOrder] = useState<RaceSortOption>(() => (searchParams.get("sort") as RaceSort | null) ?? "");
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(() => searchParams.get("view") === "calendar" ? "calendar" : readInitialViewMode());
|
||||
const [activeListTab, setActiveListTab] = useState<RaceListTab>("upcoming");
|
||||
const [isAdvancedFiltersOpen, setIsAdvancedFiltersOpen] = useState<boolean>(() =>
|
||||
@@ -263,7 +264,7 @@ export function RacesPage(): JSX.Element {
|
||||
if (distanceMin) next.set("distanceMin", distanceMin);
|
||||
if (distanceMax) next.set("distanceMax", distanceMax);
|
||||
if (searchQuery) next.set("q", searchQuery);
|
||||
if (sortOrder !== "dateAsc") next.set("sort", sortOrder);
|
||||
if (sortOrder) next.set("sort", sortOrder);
|
||||
setSearchParams(next, { replace: true });
|
||||
}, [dateFrom, dateTo, distanceMax, distanceMin, monthFilter, searchQuery, setSearchParams, sortOrder, statusFilter, viewMode, yearFilter]);
|
||||
|
||||
@@ -281,9 +282,11 @@ export function RacesPage(): JSX.Element {
|
||||
const { upcoming, completed } = useMemo(
|
||||
() => ({
|
||||
upcoming: filteredRaces.filter((race) => race.status !== "completed"),
|
||||
completed: sortByDateAsc(filteredRaces.filter((race) => race.status === "completed")),
|
||||
completed: sortOrder
|
||||
? filteredRaces.filter((race) => race.status === "completed")
|
||||
: sortByDateDesc(filteredRaces.filter((race) => race.status === "completed")),
|
||||
}),
|
||||
[filteredRaces],
|
||||
[filteredRaces, sortOrder],
|
||||
);
|
||||
const statusMessage = useMemo(() => {
|
||||
if (errorMessage && !isLoading) {
|
||||
@@ -309,7 +312,7 @@ export function RacesPage(): JSX.Element {
|
||||
dateTo,
|
||||
distanceMin,
|
||||
distanceMax,
|
||||
sortOrder !== "dateAsc" ? sortOrder : "",
|
||||
sortOrder,
|
||||
].filter(Boolean).length;
|
||||
|
||||
if (errorMessage && races.length === 0 && !isLoading) {
|
||||
@@ -478,7 +481,8 @@ export function RacesPage(): JSX.Element {
|
||||
</label>
|
||||
<label className="races-filter__field">
|
||||
<span className="races-filter__label">Сортировка</span>
|
||||
<select className="races-filter__select" value={sortOrder} onChange={(event) => setSortOrder(event.target.value as RaceSort)}>
|
||||
<select className="races-filter__select" value={sortOrder} onChange={(event) => setSortOrder(event.target.value as RaceSortOption)}>
|
||||
<option value="">По умолчанию</option>
|
||||
<option value="dateAsc">Дата: сначала ранние</option>
|
||||
<option value="dateDesc">Дата: сначала поздние</option>
|
||||
<option value="titleAsc">Название: А–Я</option>
|
||||
|
||||
Reference in New Issue
Block a user