diff --git a/apps/web/src/pages/plan.astro b/apps/web/src/pages/plan.astro index befe2bf..dc6af30 100644 --- a/apps/web/src/pages/plan.astro +++ b/apps/web/src/pages/plan.astro @@ -29,7 +29,13 @@ import Layout from "../layouts/Layout.astro"; }).slice(0, 5); }; const readPlan = (): Array> => { - try { return normalisePlan(JSON.parse(localStorage.getItem(planStorageKey) || "[]")); } + try { + const raw = localStorage.getItem(planStorageKey) || "[]"; + const normalised = normalisePlan(JSON.parse(raw)); + const serialised = JSON.stringify(normalised); + if (serialised !== raw) localStorage.setItem(planStorageKey, serialised); + return normalised; + } catch { return []; } }; const shared = new URLSearchParams(location.search).get("plan"); diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 1c58e21..b558dd7 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -35,7 +35,7 @@ footer{min-height:118px;background:var(--deep);color:#dbe4df;padding:28px max(32 @media(max-width:720px){.moderation-app{width:calc(100% - 28px)}.moderation-card{grid-template-columns:1fr}.moderation-proof{grid-row:2}.moderation-actions{grid-column:1;display:block}.moderation-actions>div{margin-top:12px}.admin-login{display:block}.admin-login button{width:100%;margin-top:12px}} @media(max-width:480px){.filters{grid-template-columns:1fr 1fr}.spot-stats strong{font-size:16px}.topbar nav{gap:4px}.topbar nav a{padding:0 5px}.brand-name strong{font-size:14px}.brand-name span{font-size:11px}.moderation-actions>div{display:grid}.moderation-actions button{width:100%}} @media(max-width:720px){.filter-advanced-fallback .filter-advanced-field{display:flex!important;align-items:center;gap:6px}.filter-advanced-fallback .filter-compact-hidden{display:flex!important}} -@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}} +@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01s!important;animation-iteration-count:1!important}} .activity-legend{display:flex;gap:10px;flex-wrap:wrap;margin:8px 0;color:#7d918b;font-size:11px}.activity-legend span:before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;background:var(--lime)}.activity-legend span:nth-child(2):before{background:#e5b95c}.activity-legend span:nth-child(3):before{background:#ef795e} /* RF4 field-guide iconography and float activity gauge */ diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a809b30..7b2b329 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -56,7 +56,7 @@ - [ ] **W05 · Crosswalk источников.** Добавлен offline-конструктор консервативных предложений: нормализуются только точные имена/алиасы, неоднозначные и unmatched строки не получают canonical key; отсутствие ID выдаётся лишь диагностикой и не считается удалением. RF4MAP/RF4-STAT directory parsers теперь преобразуются в crosswalk identities без переноса метрик или автоматической привязки. Осталось подать полный набор реальных RF4DB/RF4MAP/RF4 Posts identities и вручную подтвердить результаты, включая три ранее отмеченных отсутствующих RF4MAP объекта. - [ ] **W06 · Координаты и точность.** В `ExternalObservation`, staging, provenance опубликованного улова и публичных activity/spot-ответах добавлены `coordinate_raw`, `coordinate_precision = exact | approximate | area | missing` и список источников; RF4DB/RF4-STAT/RF4MAP/RF4 Posts parsers теперь протягивают исходную строку, включая строки без доступных числовых координат. Исправлена spot detail: точность (`точные`/`приблизительные`/`район`/`не указаны`) теперь видна рядом с координатами и покрыта smoke regression. Все public-компоненты используют единый `coordinatePrecisionLabel` с безопасным fallback и unit-регрессией четырёх состояний. In-app Chromium и focused smoke подтвердили `/spots/vyunok-321x654` на desktop/mobile без overflow; browser regression для отсутствующей точки проверяет недоступное состояние без ложных координат, а API regression параметризован по всем четырём precision-состояниям. Осталась полная browser QA матрица для всех precision/error-состояний. - [ ] **W07 · Публичный API и страницы.** API и detail-страница теперь выводят подтверждённые detail-факты водоёма: описание, уровень, количество видов, алиасы, число ссылок на точки и отдельный счётчик изображений-кандидатов; источники и непроверенные media не смешиваются. Public media API теперь передаёт reviewed `media_role` и дополнительно отбрасывает unknown/cross-entity роли на границе публикации, waterbody detail принимает только `waterbody_cover`/`waterbody_map`/`waterbody_depth_map`, а UI подписывает карту/заставку/скриншот текстом. Осталось завершить browser QA для реальных role-вариантов и не показывать abstract fingerprint как внешнее изображение. -- [ ] **W08 · Приёмка и эксплуатация.** Fixture-based parser tests, offline catalog/media audit и повторный idempotent import подтверждены: `created=0 updated=19`, в PostgreSQL ровно `19` RF4DB waterbodies плюс `2` legacy-записи, media audit сообщает `issues=[]` и `orphaned_files=[]`. In-app Chromium и focused smoke покрывают desktop/mobile основные страницы; 20.09 targeted waterbody E2E подтвердил detail с пустой активностью, not-found waterbody и not-found waterbody/fish pair, а spot regression — unavailable state без ложной координаты; публичные каталоги, media, records, spot/report/status, analytics и tackle detail имеют opt-in regression (`EXPECT_UNAVAILABLE=1`) для `503` + `Retry-After` + `no-store`, `noindex` и отсутствия утечки внутренних ошибок. Полный web E2E и visual-matrix остаются обязательным финальным прогоном; внешние detail-данные закрываются отдельно. Сетевые тесты не выполнять; регулярный импорт оставить opt-in и под общим cooldown/backoff. +- [ ] **W08 · Приёмка и эксплуатация.** Fixture-based parser tests, offline catalog/media audit и повторный idempotent import подтверждены: `created=0 updated=19`, в PostgreSQL ровно `19` RF4DB waterbodies плюс `2` legacy-записи, media audit сообщает `issues=[]` и `orphaned_files=[]`. In-app Chromium и focused smoke покрывают desktop/mobile основные страницы; 20.09 targeted waterbody E2E подтвердил detail с пустой активностью, not-found waterbody и not-found waterbody/fish pair, а spot regression — unavailable state без ложной координаты; публичные каталоги, media, records, spot/report/status, analytics и tackle detail имеют opt-in regression (`EXPECT_UNAVAILABLE=1`) для `503` + `Retry-After` + `no-store`, `noindex` и отсутствия утечки внутренних ошибок. После пересборки текущего Compose web-артефакта полный Playwright regression подтвердил `63 passed, 13 skipped` из `76` тестов, а Python suite — `229 passed, 1 skipped`; полный production/browser прогон остаётся обязательным финальным gate. Внешние detail-данные закрываются отдельно. Сетевые тесты не выполнять; регулярный импорт оставить opt-in и под общим cooldown/backoff. ### Каталог снастей, наживок и прочей оснастки