diff --git a/apps/web/src/components/ActivityEvidenceSummary.astro b/apps/web/src/components/ActivityEvidenceSummary.astro new file mode 100644 index 0000000..e83f768 --- /dev/null +++ b/apps/web/src/components/ActivityEvidenceSummary.astro @@ -0,0 +1,26 @@ +--- +import DataPassport from "./DataPassport.astro"; +import type { Activity } from "../lib/api"; +const { items, totalPoints, periodLabel } = Astro.props as { items: Activity[]; totalPoints: number; periodLabel: string }; +const sources = [...new Set(items.flatMap(item => item.sources))]; +const conflicts = [...new Set(items.flatMap(item => item.source_conflicts))]; +const observations = items.reduce((sum, item) => sum + item.catches, 0); +const confidence = items.length ? Math.round(items.reduce((sum, item) => sum + item.confidence_score, 0) / items.length) : null; +const observedAt = items.map(item => item.last_confirmed_at).filter(Boolean).sort().at(-1) ?? null; +const status = observations < 3 ? "insufficient" : "verified"; +--- +
+
+ Доверие к выборке +

Паспорт активности

+

{totalPoints} {totalPoints === 1 ? "точка" : totalPoints < 5 ? "точки" : "точек"} за {periodLabel}. Сводка построена по подтверждённым карточкам этого списка.

+
+ +
+ + diff --git a/apps/web/src/pages/fish/[slug].astro b/apps/web/src/pages/fish/[slug].astro index c4fd773..d49a0e0 100644 --- a/apps/web/src/pages/fish/[slug].astro +++ b/apps/web/src/pages/fish/[slug].astro @@ -1,5 +1,6 @@ --- import ActivityCard from "../../components/ActivityCard.astro"; +import ActivityEvidenceSummary from "../../components/ActivityEvidenceSummary.astro"; import ActivityPeriodFilter from "../../components/ActivityPeriodFilter.astro"; import EntityMedia from "../../components/EntityMedia.astro"; import AtlasBreadcrumbs from "../../components/AtlasBreadcrumbs.astro"; @@ -46,6 +47,7 @@ const schema = fish ? { "@context":"https://schema.org", "@type":"CollectionPage {fish && } {image &&
Визуальный справочник

{fish!.name_ru}

Изображение опубликовано с прямой ссылкой на источник. Оно помогает отличить вид, а актуальные точки ниже остаются отдельными наблюдениями игроков.

} + {fish && } {unavailable ? : !fish ? :
{items.length ? items.map(item => ) : }
} {fish && } diff --git a/apps/web/src/pages/waterbodies/[slug].astro b/apps/web/src/pages/waterbodies/[slug].astro index 5aa95d5..b8f9f06 100644 --- a/apps/web/src/pages/waterbodies/[slug].astro +++ b/apps/web/src/pages/waterbodies/[slug].astro @@ -1,5 +1,6 @@ --- import ActivityCard from "../../components/ActivityCard.astro"; +import ActivityEvidenceSummary from "../../components/ActivityEvidenceSummary.astro"; import ActivityPeriodFilter from "../../components/ActivityPeriodFilter.astro"; import EntityMedia from "../../components/EntityMedia.astro"; import AtlasBreadcrumbs from "../../components/AtlasBreadcrumbs.astro"; @@ -53,6 +54,7 @@ const schema = water ? { "@context":"https://schema.org", "@type":"CollectionPag {supportingMedia.length > 0 &&
{supportingMedia.map(asset =>
{mediaRoleLabel(asset.media_role ?? "")}
)}
} {water && (water.description || water.unlock_level !== undefined || water.source_url || water.source_fish_species) &&
Канонические сведения

{water.name_ru}

{water.description &&

{water.description}

}{water.unlock_level !== undefined && water.unlock_level !== null &&

Открывается с уровня: {water.unlock_level}

}{water.source_fish_species?.length &&

В карточке источника указано видов рыб: {water.source_fish_species.length}.

}{water.source_aliases?.length &&

Алиасы источника: {water.source_aliases.join(", ")}.

}
Источник{sourceHref ?

{water.source_system ?? "Внешний источник"} · исходная карточка

:

Источник для описания ещё не подтверждён.

}{water.source_point_urls?.length &&

Сохранено ссылок на точки: {water.source_point_urls.length}.

}{water.source_image_urls?.length &&

Изображений-кандидатов: {water.source_image_urls.length}; публикация требует отдельной проверки.

}{water.source_checked_at &&

Проверено: {new Date(water.source_checked_at).toLocaleDateString("ru-RU")}

}
} {water && } + {water && } {unavailable ? : !water ? :
{items.length ? items.map(item => ) : }
} {water && } diff --git a/apps/web/src/pages/waterbodies/[slug]/[fish].astro b/apps/web/src/pages/waterbodies/[slug]/[fish].astro index d4ed89b..a752fbf 100644 --- a/apps/web/src/pages/waterbodies/[slug]/[fish].astro +++ b/apps/web/src/pages/waterbodies/[slug]/[fish].astro @@ -1,5 +1,6 @@ --- import ActivityCard from "../../../components/ActivityCard.astro"; +import ActivityEvidenceSummary from "../../../components/ActivityEvidenceSummary.astro"; import ActivityPeriodFilter from "../../../components/ActivityPeriodFilter.astro"; import AtlasBreadcrumbs from "../../../components/AtlasBreadcrumbs.astro"; import PageHero from "../../../components/PageHero.astro"; @@ -40,6 +41,7 @@ const schema = valid ? { "@type":"CollectionPage", name:`${fish!.name_ru} на $ {valid && } + {valid && } {unavailable ? : !valid ? :
{items.length ? items.map(item => ) : }
} {valid && } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index bacc827..61d3e64 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -94,7 +94,7 @@ - [ ] **U01 · UX-контракт и scorecard.** Контракт маршрутов, смысловой порядок ответа, словарь статусов и целевые метрики зафиксированы в [ux-contract.md](ux-contract.md); для home, spot, waterbody, plan и tackle описаны первый ответ и обязательное объяснение. Сквозной Playwright task journey проверяет home → spot → «Что взять» → plan и возврат query-контекста, включая source/evidence-поля первой карточки; ручной сценарий и критерии зафиксированы в [ux-task-review-2026-09-21.md](ux-task-review-2026-09-21.md). Осталось провести review с участниками и собрать completion rate на пилоте. - [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Главная сохраняет рыбу, водоём, период и сортировку в shareable URL, явно показывает контекст запроса и даёт текстовый CTA «Открыть точку» на каждой карточке, включая mobile; первый экран ограничен пятью вариантами, а остальные доступны через сохраняющую query-контекст серверную пагинацию; empty-state предлагает вернуться к полному набору данных. Каталоги рыбы и водоёмов имеют поиск по названию, сохраняют `q` в URL и пересчитывают пагинацию по отфильтрованному набору. Добавлен URL-сохраняющий режим `view=map` с отдельным mobile-friendly действием: он показывает только подтверждённые `x:y` на честной координатной схеме и ведёт в detail точки, без имитации геометрии водоёма. Detail-страницы рыбы, водоёма и пары теперь позволяют выбрать окно 24/72 часа/7 дней, передают его по связанным ссылкам и используют серверную пагинацию результатов с общим счётчиком; stale-offset автоматически возвращается на последнюю доступную страницу. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. -- [ ] **U03 · Evidence/trust card.** Общий evidence-контракт используется на activity-карточках, detail точки, водоёма и карточках снастей: freshness с текстовым `Свежо`/`Устарело`, явный период расчёта, completeness, confidence/статус, source badges и доступные доменные поля; при выборке меньше 3 явно показано отдельное состояние `Недостаточно данных`, ограниченный источник не смешивается с неполными полями, а явно переданные provenance-конфликты видны текстом. Источники координат доступны в раскрываемом блоке паспорта, тогда как точность остаётся видимой в основном ответе. Targeted E2E и API-тест проверяют публичные пути, период, 48-часовой порог и конфликт источников; единый `passportDisplayStatus` дополнительно закрепляет приоритет `conflict` → `stale` → исходный статус unit-регрессией. Все публичные provenance links проходят credential-free HTTP(S) allowlist. +- [ ] **U03 · Evidence/trust card.** Общий evidence-контракт используется на activity-карточках, detail точки, водоёма и карточках снастей; detail-страницы рыбы, водоёма и пары дополнительно показывают единый сводный паспорт активности с периодом, общим числом точек, наблюдениями текущей выборки, средним доверием, свежестью, источниками и конфликтами. Freshness имеет текстовый `Свежо`/`Устарело`, completeness, confidence/статус и доступные доменные поля; при выборке меньше 3 явно показано отдельное состояние `Недостаточно данных`, ограниченный источник не смешивается с неполными полями, а явно переданные provenance-конфликты видны текстом. Источники координат доступны в раскрываемом блоке паспорта, тогда как точность остаётся видимой в основном ответе. Targeted E2E и API-тест проверяют публичные пути, период, 48-часовой порог и конфликт источников; единый `passportDisplayStatus` дополнительно закрепляет приоритет `conflict` → `stale` → исходный статус unit-регрессией. Все публичные provenance links проходят credential-free HTTP(S) allowlist. - [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Источники координат в activity/spot evidence-паспорте и многокомпонентный комплект снастей в списке уловов раскрываются по запросу, не скрывая основную точность, статус и источник; canonical-компоненты сохраняют переходы в каталог. Режим `view=map` использует единый URL-контракт, на mobile открывается отдельным действием и прямо маркирован как координатная схема, пока геометрия водоёма не подтверждена. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу. - [ ] **U05 · Mobile-first и сохранённый план рыбалки.** `/plan` поддерживает список до 5 уникальных локальных вариантов, удаление, очистку, переход к точке, print/PDF и восстановление из shareable URL; кнопка «Поделиться планом» использует native share или clipboard fallback, отдельное копирование ссылки и явно сообщает об успехе, отмене, пустом плане или повреждённой share-ссылке. Detail-кнопка сохраняет данные с `aria-pressed` и восстанавливается после reload. Print/mobile-контракт проверяет 320/390 px, лимит импорта и отсутствие горизонтального overflow; импорт дедуплицирует точки, нормализует длины локальных текстовых полей, принимает confidence только в диапазоне `0..100` и не теряет реальное значение `0`, а карточка плана сравнивает подтверждённые метод/проводку и явно показывает риск по выборке/уверенности. - [ ] **U06 · Контентная и визуальная иерархия.** Для detail точки действие «Что взять» выделено отдельным заголовком, provenance и качество собраны в общем паспорте данных, а подтверждённые метод/проводка теперь вынесены в отдельный блок рядом с последними уловами; статусы дополнительно передаются текстом, малая выборка не маркируется как готовая рекомендация, а дублирующие catches/players/confidence убраны из вторичной колонки activity-карточки. Копирование координат имеет Clipboard API fallback. Mobile route-contract проверяет первый ответ и действие на home, spot, waterbody, plan и tackle, а task regression требует source/evidence-поля; остаётся ручной review визуальных деталей по [ux-task-review-2026-09-21.md](ux-task-review-2026-09-21.md).