Метки времени в SQLite переведены с формата datetime('now')
(«2006-01-02 15:04:05») на RFC 3339 всегда-UTC («2006-01-02T15:04:05Z»):
самоописываемое хранилище (зона в значении), валидный ISO 8601, единый
формат с логами. Фиксированная ширина сохраняет лексикографическую
сортировку TEXT = хронологию (COALESCE(source_added_at, created_at)).
- Единая точка генерации времени в Go: store.Now()/FormatTime; DEFAULT
(datetime('now')) снят со всех колонок — время всегда пишет приложение
(зеркально ident.NewID для id), fail-loud при забытой вставке (NOT NULL).
Все INSERT-сайты в store передают created_at/updated_at явно.
- Миграция 0008 (rebuild 7 таблиц без DEFAULT + backfill strftime, FK/PK/
индексы сохранены байт-в-байт по образцу 0006); симметричная down.
- Новая секция конфига [general] с полем timezone (дефолт UTC) — зона
ОТОБРАЖЕНИЯ в веб-UI; хранение остаётся UTC. Жёсткая валидация зоны на
старте; zoneinfo встроен (time/tzdata), заменён зашитый Europe/Moscow.
- Тесты: round-trip миграции (up/down, NULL source_added_at), валидация
зоны, сдвиг даты по зоне; обновлены фикстуры и TestUlidMigration.
- Docs: конвенции database/config, ER-схема; спека web-ui (таймзона).
OpenSpec change time-storage-rfc3339 (заархивирован).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
5.8 KiB
Markdown
77 lines
5.8 KiB
Markdown
## 1. Ревью дизайна (чекпоинт ДО кода)
|
||
|
||
- [x] 1.1 Согласовать design.md: стратегию миграции/backfill, снятие `DEFAULT`,
|
||
секцию `[general]` с `timezone`. Правки внести до реализации.
|
||
|
||
## 2. Хелперы формата времени (store)
|
||
|
||
- [x] 2.1 `internal/store/download.go`: убрать `sqliteTimeLayout`; `FormatTime` →
|
||
`t.UTC().Format(time.RFC3339)`, `ParseTime` → `time.Parse(time.RFC3339, s)`.
|
||
Обновить комментарии (формат `...Z`, не `datetime('now')`), включая коммент про
|
||
`SourceAddedAt` на `download.go:118` (ссылается на `sqliteTimeLayout`).
|
||
- [x] 2.2 Добавить `store.Now()` (обёртка над `time.Now().UTC()`) как единую
|
||
точку получения «сейчас» в слое store.
|
||
- [x] 2.3 Проверить `migrations/0006_ulid_identity.go:204` (`parseCreatedAt`
|
||
парсит старый формат) — это историческая миграция, формат менять не нужно;
|
||
убедиться, что она не ломается и не пересекается с новой.
|
||
|
||
## 3. Убрать DEFAULT и писать время из Go
|
||
|
||
- [x] 3.1 Каждый INSERT-**сайт** со временем явно передаёт `created_at`/`updated_at`
|
||
(= `FormatTime(store.Now())`). Полный список сайтов (не методов!):
|
||
`download.go` — `CreateDownloadIfNoActive` (вставка download :219 + вставка
|
||
хешей :228), dedup top-up хешей :203 (`INSERT OR IGNORE`), `AddInfohashes` :306
|
||
(`INSERT OR IGNORE`); `recognition.go` — `CreateRecognition` :72, `AddHint` :108,
|
||
`SetOverride` :130, `AddFileLink` :185, `AddMetadataCandidate` :332. Для
|
||
`INSERT OR IGNORE` в `download_infohash` `created_at` обязателен на новой строке
|
||
(после снятия `DEFAULT` + `NOT NULL`); при конфликте PK строка не вставляется — ок.
|
||
- [x] 3.2 `download.go:510`: инлайн `updated_at = datetime('now')` → параметр
|
||
`FormatTime(store.Now())` (sortability-ловушка — проверить, что не осталось
|
||
сырых `datetime('now')` в SQL: `grep -rn "datetime('now')" internal`).
|
||
|
||
## 4. Миграция БД (goose)
|
||
|
||
- [x] 4.1 Новая миграция (стиль `0006`): backfill каждой timestamp-колонки
|
||
`strftime('%Y-%m-%dT%H:%M:%SZ', col)` (7 таблиц: download.created_at/updated_at/
|
||
source_added_at, download_infohash/recognition/hint/override/metadata_candidate/
|
||
file_link .created_at) + rebuild таблиц без `DEFAULT` на timestamp-колонках.
|
||
- [x] 4.2 Down-миграция: обратный backfill `strftime('%Y-%m-%d %H:%M:%S', col)` +
|
||
восстановление `DEFAULT`.
|
||
- [x] 4.3 Тест миграции: строки со старым форматом → новый формат; хронология
|
||
сортировки сохранена; `source_added_at IS NULL` не затронут; обратный backfill
|
||
(down) на строке с `T`/`Z` даёт старый формат.
|
||
- [x] 4.4 Обновить существующий `TestUlidMigration` (`migration_test.go:100-101`):
|
||
ассерт ждёт старый формат `2026-01-01 10:00:00`, после новой миграции метки
|
||
станут `...T...Z` — поправить ожидаемое (заодно канарейка, что backfill отработал).
|
||
|
||
## 5. Настройка таймзоны отображения
|
||
|
||
- [x] 5.1 `internal/config`: секция `[general]` (`General struct`) с полем
|
||
`Timezone string` (дефолт `UTC`) + распарсенный `*time.Location`; жёсткая
|
||
валидация на старте через `time.LoadLocation`, невалидная зона → ошибка
|
||
конфигурации (без рантайм-фолбэка).
|
||
- [x] 5.1a Встроить zoneinfo: `import _ "time/tzdata"` в точке входа
|
||
(`cmd/jellybit`). Тогда `LoadLocation` падает только на битом имени.
|
||
- [x] 5.2 `internal/httpapi`: пробросить `*time.Location` до `fmtDate`/`humanizeAge`
|
||
(свободные функции — тянуть параметром через `toView`, вызовы `httpapi.go:288,545`,
|
||
либо сделать методами `server`); заменить зашитый `mskLoc` (`live.go:189-201`)
|
||
целиком (после встраивания tzdata рантайм-фолбэк на UTC внутри `mskLoc` не нужен).
|
||
Guard: `t.In(nil)` паникует — при незаданной зоне подставлять `time.UTC`; тест-хелпер
|
||
`testRouterLive` тоже задаёт loc.
|
||
- [x] 5.3 Обновить пример конфига (`config.example.toml`/деплой): секция
|
||
`[general]` первой, `timezone`; для этого сервера — `Europe/Moscow`.
|
||
|
||
## 6. Документация
|
||
|
||
- [x] 6.1 `docs/conventions/database.md:42-43`: формат меток → RFC 3339 (UTC,
|
||
`Z`), генерация только из Go (`store.Now`/`FormatTime`), без `DEFAULT`.
|
||
- [x] 6.2 `docs/conventions/config.md`: зона отображения конфигурируема (дефолт
|
||
UTC); хранение и логи — UTC.
|
||
- [x] 6.3 `docs/specs/database.md`: комментарий формата timestamp-колонок в ER.
|
||
|
||
## 7. Проверка и ревью кода (чекпоинт до archive)
|
||
|
||
- [x] 7.1 `task test` / `task lint` зелёные; тест сортировки списка на смешанных
|
||
и однотипных метках.
|
||
- [x] 7.2 Ревью кода реализации; затем `openspec validate --strict` и archive.
|