docs: описание релиза Galo за 21 октября 2026

Описание изменений с main (84ce4d1) до develop (afe85f7): локальный
режим на SQLite и файловой системе, слой объектов, редактирование фото,
новый интерфейс рабочего пространства, видео, Android-клиент, профиль,
офлайн, живое обновление, исправления и инструкция по обновлению.
This commit is contained in:
iu
2026-10-21 21:15:59 +03:00
parent afe85f7be0
commit 723b19bbe5
@@ -0,0 +1,280 @@
# Описание релиза Galo — 21 октября 2026
**База релиза:** `main` (`84ce4d1`, 10 октября 2026) → `develop` (`afe85f7`, 21 октября 2026)
**Объём:** 138 коммитов, 4 спецификации обновлены, 1 изменение реализовано
---
## Коротко о главном
1. **Локальный режим без внешних сервисов.** Слой метаданных переключается на
встроенный SQLite, хранилище блобов — на локальную файловую систему. По умолчанию
приложение работает полностью на одной машине: без Postgres и без S3.
2. **Редактирование фотографий в браузере.** Вкладка «Редактирование» с инструментом
«Свет»: экспозиция, контраст, света, тени, белые и чёрные, тональная кривая по
каналам с живой гистограммой. Правки видны сразу, без ожидания сервера.
3. **Новый интерфейс рабочего пространства.** Альбомы с обложками, тёмная, светлая и
нейтральная темы, единая система векторных иконок, гало-эмблема и фавикон.
4. **Нативный Android-клиент.** Отдельное приложение: просмотр альбомов, загрузка с
устройства, двусторонняя синхронизация с сервером, безопасная разгрузка превью.
5. **Видео и офлайн.** Видео в общих альбомах с фоновой подготовкой и перемоткой;
офлайн-кеш оболочки, списков и превью.
---
## Новые возможности
### Локальный режим: SQLite и файловая система
- **Слой метаданных** выбирается конфигурацией: внешний PostgreSQL или встроенный
SQLite с тем же поведением для одного экземпляра приложения.
- **Хранилище блобов** выбирается отдельно: S3-совместимое (Garage) или локальная
файловая система. Клиент никогда не видит хранилище — блобы всегда отдаются через
бэкенд по подписанному URL.
- По умолчанию (`DB_BACKEND=sqlite`, `STORAGE_BACKEND=fs`) приложение поднимается без
Postgres и S3: данные лежат в `galo.db` и каталоге `data/`.
- **Секрет подписи JWT и URL объектов** генерируются при первом старте и хранятся в
слое метаданных — переживают перезапуск и рестарт SQLite.
- Реконсиляция хранилища с журналом объектов: `make reconcile` (отчёт),
`make reconcile-delete` (удаление осиротевших блобов). Сборку мусора ведёт журнал,
хранилище не сканируется.
### Слой объектов
- Физическое объектное хранилище с **непрозрачными ключами**: идентификаторы альбомов и
фото в ключах не участвуют, сопоставление живёт только в метаданных.
- **Журнал объектов**: у каждого объекта ровно одна запись с ключом, MIME-типом,
размером, габаритами и состоянием; фото ссылается на объекты по ролям
(`original`, `thumb`, `medium`, `preview`, `playback`, `edit`, `edit_thumb`,
`edit_medium`, `edit_preview`).
- Один физический объект может разделяться несколькими фото: перенос и копирование
меняют только ссылки, хранилище не трогается.
- Сборка мусора по ссылкам: объект удаляется, когда на него не ссылается ни одно фото
и ни один аватар.
### Редактирование фотографий
- Вкладка «Редактирование» в правой панели фото, инструмент «Свет»: экспозиция,
контраст, света, тени, белые, чёрные.
- **Тональная кривая**: выбор канала (R/G/B/мастер), точки добавляются кликом по
полю графика, перетаскиваются и двигаются стрелками с клавиатуры.
- **Гистограмма каналов** пересчитывается на лету с учётом текущих правок.
- Блок «Кривая» сворачивается (по умолчанию свёрнут), кнопки выбора канала остаются
доступными.
- Предпросмотр правок рисуется **в браузере** — WASM-модулем на Rust, с резервной
JS-реализацией, если модуль недоступен или считает иначе.
- Правки применяются сразу; на сервер уходят через 10 секунд без изменений или при
переключении фото. Сервер рендерит отредактированную версию в фоне отдельной
очередью и сообщает о готовности.
- Переключатель «Сброс» удаляет и параметры правок, и отредактированную версию.
- Редактирование неразрушающее: оригинал остаётся неизменным, скачивание отдаёт
исходник. Рендер идёт по исходному объекту, а не по уменьшенной копии.
### Интерфейс рабочего пространства
- Альбомы с обложками, «лайтрумовская» компоновка: рельс альбомов, сетка, единая
панель фотографии.
- Три темы — тёмная, светлая, нейтральная — с семантическими токенами; переключатель
тем переехал в шапку, нейтральная тема затемнена.
- **Единая система иконок**: встроенная векторная графика (Material Symbols) вместо
эмодзи и символов системного шрифта — иконки наследуют цвет темы и не тянут
внешние ресурсы.
- **Гало-эмблема** (диск солнца в отделённом кольце) и фирменный фавикон: отдаются
самим приложением, наследуют тему, читаются в размере 16–24 px.
- Панель сведений о фото со **сворачиваемыми секциями** и настраиваемым набором
атрибутов: состав EXIF хранится в профиле пользователя.
- Полноэкранный просмотр: своя панель, кнопка «на весь экран», возврат по `Esc`.
### Просмотр и навигация
- **Зум** колесом мыши (с привязкой к курсору, до 200%), клавишами `+` и `-`;
`Space` возвращает во вписанный вид, а из вписанного — в 100% от оригинала по
центру кадра.
- В зуме стрелки клавиатуры **панорамируют** фото, переключение соседних снимков
остаётся за мышью и кнопками у краёв.
- **Скрытие боковых панелей** альбома: кнопка в панели сетки и хоткей `Tab`. Панели
уезжают за края, содержимое занимает всю ширину, панели возвращаются при наведении
на кромку окна. Размер миниатюр при этом сохраняется — в строке помещается больше
фото.
- Лента миниатюр в просмотре следует за текущим фото; на превью работает наведение-
зум.
- Полноэкранный режим приложения по `Ctrl+F`, навигация с клавиатуры в сетке с
возвратом фокуса, открытие фото тапом на тач-экранах.
- Просмотр 360-панорам по тегу `pano360`, корректное направление перетаскивания.
- Пока грузится оригинал для зума — спиннер; на Android — щипок-зум.
### Работа с фотографиями
- Мультивыбор в сетке, контекстные меню, **перенос и копирование** снимков, корзина
на уровне альбома и общие счётчики.
- **Теги** фото: правки расходятся по сетке и просмотру.
- Переименование фото и удаление из просмотра с подтверждением.
- EXIF извлекается полностью при загрузке; для старых снимков — дозаполнение
(`cmd/backfill-exif`).
### Офлайн-режим
- Service worker кеширует оболочку приложения, списки и эскизы/превью фотографий:
открытые альбомы и ранее загруженные снимки доступны без сети.
- Модуль фильтров в офлайне не кешируется — он всегда подтягивается свежим.
### Видео
- Загрузка видео в те же альбомы, что и фото (`kind=video`).
- Фоновая подготовка: постер (кадр кадра → обычные варианты сетки) и web-совместимый
вариант для воспроизведения (ремукс без перекодирования либо транскод в
h264/yuv420p/aac с `+faststart`), с поддержкой перемотки (`Range`).
- Транскод идёт в отдельном малом пуле и не вытесняет генерацию вариантов
изображений; готовность транслируется подписчикам альбома.
- Без `ffmpeg`/`ffprobe` приём видео отклоняется, `/api/health` сообщает
`"video": "unavailable"`, изображения работают как прежде.
### Android-клиент
- Отдельный Gradle-модуль, работает с существующим HTTP API, сервер и веб не
трогает. Kotlin + Jetpack Compose, `minSdk 28`, `targetSdk 35`.
- **Двусторонняя синхронизация** альбома устройства с серверным альбомом: загрузки,
изменения состава, удаления.
- **Безопасная разгрузка**: с устройства уходит превью, оригинал остаётся на сервере;
потеря серверной копии распознаётся и показывается в интерфейсе.
- Статус синхронизации на карточке альбома, ход восстановления оригиналов,
сортировка списка, обновление списка альбомов в реальном времени.
- Установка на устройство одной командой: `make install-android-app` (с выбором
серийного номера при нескольких подключённых устройствах).
### Профиль и учётная запись
- Профиль пользователя: круглый аватар с загрузкой и кадрированием, публичная отдача
аватара по версионированной ссылке.
- Вход и регистрация по телефону (E.164) в дополнение к email, управление
идентификаторами, удаление аккаунта.
- Предпочтения пользователя (в том числе состав панели сведений) хранятся на сервере.
### Альбомы и живое обновление
- Обложки альбомов: отдельный жизненный цикл, обложка доводится до готовности и
доставляется подписчикам.
- Живое обновление: события жизненного цикла альбома и изменения состава фото через
SSE, подписчики видят изменения без перезагрузки.
- Квадратный вариант `medium` — резкие миниатюры в сетке; шаг переключения размера
миниатюр и направление ползунка приведены в порядок.
- Быстрая загрузка больших альбомов.
### Наблюдаемость и эксплуатация
- Настраиваемое логирование: `LOG_LEVEL` (`debug` включает access-логи и ожидаемые
4xx), `LOG_FORMAT` (`json`/`text`), request id в ответах и логах.
- `scripts/seed.py` и `make testdata` — наполнение запущенного инстанса тестовыми
данными через публичный API.
- Release-сборка: `make build-release` — статичный бинарник с вшитой версией и
UPX-сжатием, если он установлен.
---
## Исправления
- **Копия фото больше не наследует отредактированную версию.** Раньше копирование
переносило все привязки объектов, включая `edit*`: копия показывала
отредактированную картинку, но не наследовала параметры правок, и первая же новая
правка перезаписывала обработку с нуля. Теперь правка принадлежит своему фото, а
копия показывает исходник. Разовая миграция убирает привязки у копий, созданных до
фикса.
- Предпросмотр правок работает и без зума, а не только в увеличенном виде.
- Устранено искажение цветов в предпросмотре (WASM-фильтры считались по
преобразованным каналам).
- Гистограмма пересчитывается при правке, а не остаётся от исходного снимка.
- Линия тональной кривой видима в тёмной теме.
- Починен зум в просмотре: контейнер прокрутки и центрирование.
- Android: разгруженное превью больше не считается новой версией элемента.
- Сервер отдаёт контрольную сумму оригинала в ответе на загрузку — клиент может
сверить файл.
- Счётчик корзины и правки тегов синхронизируются с сеткой и просмотром.
- Ошибка загрузки крупного файла показывается явно, лимит поднят.
- `make run` пересобирает фронтенд, если он изменился.
---
## Технические изменения
### API
Новые эндпоинты:
- `PATCH /api/photos/:id/edit` — сохранить параметры правок (тело: `{"light": {...}}`).
- `DELETE /api/photos/:id/edit` — сбросить правки вместе с отредактированной
версией.
- `GET|PATCH|DELETE /api/auth/me`, `POST /api/auth/me/password`,
`POST /api/auth/me/identifiers`, `PATCH /api/auth/me/preferences`,
`POST|DELETE /api/auth/me/avatar`, `GET /api/avatars/:userID/:version?size=large`.
- `POST /api/photo-batch/copy`, `POST /api/photo-batch/move` — пакетные операции над
снимками.
Изменившиеся ответы: у фото появились поля `edit_params`, `edit_url`,
`edit_thumb_url`, `edit_medium_url`, `edit_preview_url`; у видео — `playback_url`.
### Схема данных и миграции
- Postgres: миграции до `000016`; SQLite: до `000008` (нумерация диалектов
независимая, миграции вшиты в бинарник и применяются автоматически при старте;
`make migrate` применяет только Postgres).
- Новые роли объектов `edit`, `edit_thumb`, `edit_medium`, `edit_preview` — рядом с
обычными `original`, `thumb`, `medium`, `preview`, `playback`.
- Новая колонка `photos.edit_params` — параметры неразрушающих правок.
### Сборка
- Фильтры правок считает WASM-модуль из `frontend/wasm/light` (Rust, `no_std`).
Пересборка нужна только при правке самого модуля; в репозитории лежит готовый
`light.wasm`, поэтому фронтенд собирается без Rust-инструментов.
- `npm run wasm` — отдельная пересборка модуля; обычная сборка фронтенда обновляет
его сама.
---
## Как обновиться
1. Собрать бинарник: `make build` (фронтенд → `backend/static` → `go:embed` → `bin/galo`)
или `make build-release` — с вшитой версией.
2. Остановить сервис, заменить бинарник. Фронтенк вшит в бинарник, отдельно
выкладывать нечего.
3. Запустить: миграции применятся автоматически при старте для выбранного бэкенда.
Для Postgres можно отдельно прогнать `make migrate`.
4. Для видео нужен `ffmpeg`/`ffprobe` на хосте; без них приём видео отключается.
5. Android-клиент ставится отдельно: `make install-android-app`.
Откат: бинарник возвращается предыдущей версии; миграции `down` не восстанавливают
удалённые привязки копий — это безопасно, отредактированные версии исходных фото
остаются на месте.
---
## Ограничения
- Редактирование — только для изображений; видео не редактируется.
- Правки не порождают отдельной записи в истории: параметры хранятся в метаданных
фото, отредактированная версия — как отдельный объект.
- Офлайн-кеш ограничен оболочкой, списками и эскизами/превью: оригиналы для зума и
скачивания требуют сети.
- Android-клиент требует JDK 17 и Android SDK (`platforms;android-35`,
`build-tools;35.0.0`).
- Номер версии берётся из `git describe --tags`; в репозитории пока нет тегов, поэтому
версия в сборке — короткий SHA или `dev`. Для осмысленной нумерации нужен тег.
---
## Для разработчиков
- Спецификации OpenSpec синхронизированы с поведением: обновлены `ui-workspace`
(редактирование, кривая, зум и навигация), `media-objects` (роли `edit*`, раздельное
владение правками, отдельная очередь рендера), `photos` (рендер по оригиналу),
`dev-make-workflow` (сборка WASM-модуля).
- Изменение `fix-copy-edit-variant-sharing` реализовано (9/9 задач) и ждёт ревью и
архивации: `openspec list` показывает его как `complete`.
- Контрактные тесты репозиториев (`internal/repo/contract`) гоняют один набор
проверок на SQLite и Postgres — новый кейс `Clone` проверяет, что копия не
наследует отредактированную версию, в обоих хранилищах.
- Интеграционные тесты Postgres требуют `TEST_DATABASE_URL`; без них они
пропускаются. Тесты e2e поднимают SQLite в `t.TempDir()` и внешних сервисов не
требуют.
- Сервис переименован в `galo`, бренд — «гало»; сборка `make build-release` пишет
`bin/galo-$(VERSION)`.