From 196185dc9786aee8a4bd930e41cc9f09ba8e8f41 Mon Sep 17 00:00:00 2001 From: IK Date: Sun, 20 Sep 2026 20:29:41 +0700 Subject: [PATCH] feat: show evidence analysis periods --- apps/web/src/components/ActivityCard.astro | 4 ++-- apps/web/src/components/DataPassport.astro | 6 +++--- apps/web/src/pages/fish/[slug].astro | 2 +- apps/web/src/pages/index.astro | 2 +- apps/web/src/pages/waterbodies/[slug].astro | 2 +- apps/web/src/pages/waterbodies/[slug]/[fish].astro | 2 +- apps/web/tests/evidence-card.spec.ts | 1 + docs/ROADMAP.md | 2 +- 8 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/ActivityCard.astro b/apps/web/src/components/ActivityCard.astro index 14157ba..2485e4f 100644 --- a/apps/web/src/components/ActivityCard.astro +++ b/apps/web/src/components/ActivityCard.astro @@ -5,7 +5,7 @@ import FishingIcon from "./FishingIcon.astro"; import DataPassport from "./DataPassport.astro"; import FishSilhouette from "./FishSilhouette.astro"; import TackleGlyph from "./TackleGlyph.astro"; -const { item } = Astro.props as { item: Activity }; +const { item, periodLabel = null } = Astro.props as { item: Activity; periodLabel?: string | null }; const level = activityLevel(item.activity_score); const limited = item.catches < 3; const precision = { exact: "точные", approximate: "приблизительные", area: "район", missing: "не указаны" }[item.coordinate_precision]; @@ -18,7 +18,7 @@ const precision = { exact: "точные", approximate: "приблизител
{item.x}:{item.y} · {precision} {ago(item.last_confirmed_at)}

{item.explanation}

- +
{limited ? "Нужно ещё подтверждений" : item.best_bait ? "Работает сейчас" : "Наживка не указана"}{item.best_bait ?? "не указана"}
{item.catches}{plural(item.catches, ["улов", "улова", "уловов"])}
{item.unique_players}{plural(item.unique_players, ["игрок", "игрока", "игроков"])}
{kg(item.average_weight_g)}средний вес
{item.confidence_score}%уверенность
diff --git a/apps/web/src/components/DataPassport.astro b/apps/web/src/components/DataPassport.astro index 3d85f61..a006e58 100644 --- a/apps/web/src/components/DataPassport.astro +++ b/apps/web/src/components/DataPassport.astro @@ -1,8 +1,8 @@ --- import SourceBadge from "./SourceBadge.astro"; import { ago, freshnessStatus } from "../lib/api"; -type Props = { sources: string[]; sourceUrl?: string | null; observedAt?: string | null; completeness?: number | null; confidence?: number | null; sampleSize?: number | null; independentPlayers?: number | null; coordinatePrecision?: "exact" | "approximate" | "area" | "missing" | null; status?: "verified" | "unverified" | "incomplete" }; -const { sources, sourceUrl, observedAt, completeness = null, confidence = null, sampleSize = null, independentPlayers = null, coordinatePrecision = null, status = "verified" } = Astro.props as Props; +type Props = { sources: string[]; sourceUrl?: string | null; observedAt?: string | null; periodLabel?: string | null; completeness?: number | null; confidence?: number | null; sampleSize?: number | null; independentPlayers?: number | null; coordinatePrecision?: "exact" | "approximate" | "area" | "missing" | null; status?: "verified" | "unverified" | "incomplete" }; +const { sources, sourceUrl, observedAt, periodLabel = null, completeness = null, confidence = null, sampleSize = null, independentPlayers = null, coordinatePrecision = null, status = "verified" } = Astro.props as Props; const freshness = freshnessStatus(observedAt); const displayStatus = status === "verified" && freshness === "stale" ? "stale" : status; const statusLabels = { verified: "Учтено", unverified: "Ждёт проверки", incomplete: "Неполные данные", stale: "Данные устарели" }; @@ -12,6 +12,6 @@ const precisionLabels = { exact: "точные", approximate: "приблизи
Паспорт данных{statusLabels[displayStatus]}
{sources.map(source => )}
-
Свежесть
{freshness === "stale" ? "Устарело" : freshness === "fresh" ? "Свежо" : "Не указана"}{observedAt && ` · ${ago(observedAt)}`}
Полнота
{completenessLabel}
Доверие
{confidence == null ? "После проверки" : `${confidence}%`}
{sampleSize != null &&
Наблюдения
{sampleSize}
}{independentPlayers != null &&
Игроки
{independentPlayers}
}{coordinatePrecision &&
Координаты
{precisionLabels[coordinatePrecision]}
}
+
Свежесть
{freshness === "stale" ? "Устарело" : freshness === "fresh" ? "Свежо" : "Не указана"}{observedAt && ` · ${ago(observedAt)}`}
{periodLabel &&
Период
{periodLabel}
}
Полнота
{completenessLabel}
Доверие
{confidence == null ? "После проверки" : `${confidence}%`}
{sampleSize != null &&
Наблюдения
{sampleSize}
}{independentPlayers != null &&
Игроки
{independentPlayers}
}{coordinatePrecision &&
Координаты
{precisionLabels[coordinatePrecision]}
}
{sampleSize != null && sampleSize < 3 &&

Минимум для рекомендации: 3 наблюдения.

}
diff --git a/apps/web/src/pages/fish/[slug].astro b/apps/web/src/pages/fish/[slug].astro index 52deab0..25ae939 100644 --- a/apps/web/src/pages/fish/[slug].astro +++ b/apps/web/src/pages/fish/[slug].astro @@ -30,5 +30,5 @@ const schema = fish ? { "@context":"https://schema.org", "@type":"CollectionPage {image &&
Визуальный справочник

{fish!.name_ru}

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

} - {unavailable ? : !fish ? :
{items.length ? items.map(item => ) : }
} + {unavailable ? : !fish ? :
{items.length ? items.map(item => ) : }
} diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro index e0be486..fd4d4de 100644 --- a/apps/web/src/pages/index.astro +++ b/apps/web/src/pages/index.astro @@ -94,7 +94,7 @@ const datasetJsonLd = {
Ваш запрос

{queryTitle}

{periodLabel} · {sortLabel}

{selectedWaterbody}{selectedFish}{periodLabel}{sortLabel}{filtersChanged && Сбросить запрос}
-
За выбранный период

Горячие точки

{items.length} на странице · всего {totalItems}
{filterError ?

Некорректные фильтры

Выберите период и сортировку из предложенных значений.

Сбросить фильтры
: activityUnavailable ? : items.length ?
{items.map(item => )}
:

Пока нет свежих данных

Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.

{filtersChanged && Показать все данные}
}
+
За выбранный период

Горячие точки

{items.length} на странице · всего {totalItems}
{filterError ?

Некорректные фильтры

Выберите период и сортировку из предложенных значений.

Сбросить фильтры
: activityUnavailable ? : items.length ?
{items.map(item => )}
:

Пока нет свежих данных

Для выбранных фильтров нет одобренных наблюдений. Расширьте период или выберите другой водоём.

{filtersChanged && Показать все данные}
}
{items[0] && leaderLevel && }
{!filterError && !activityUnavailable && } diff --git a/apps/web/src/pages/waterbodies/[slug].astro b/apps/web/src/pages/waterbodies/[slug].astro index ce1edfe..68ba258 100644 --- a/apps/web/src/pages/waterbodies/[slug].astro +++ b/apps/web/src/pages/waterbodies/[slug].astro @@ -33,5 +33,5 @@ const schema = water ? { "@context":"https://schema.org", "@type":"CollectionPag {image &&
Карта и образ водоёма

{water!.name_ru}

Материал показан с прямой атрибуцией. Координаты активных точек ниже относятся к данным наблюдений, а не к геометрии изображения.

} {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(", ")}.

}
Источник{water.source_url ?

{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 && } - {unavailable ? : !water ? :
{items.length ? items.map(item => ) : }
} + {unavailable ? : !water ? :
{items.length ? items.map(item => ) : }
} diff --git a/apps/web/src/pages/waterbodies/[slug]/[fish].astro b/apps/web/src/pages/waterbodies/[slug]/[fish].astro index e84dba5..e663c43 100644 --- a/apps/web/src/pages/waterbodies/[slug]/[fish].astro +++ b/apps/web/src/pages/waterbodies/[slug]/[fish].astro @@ -24,5 +24,5 @@ const schema = valid ? { "@type":"CollectionPage", name:`${fish!.name_ru} на $ - {unavailable ? : !valid ? :
{items.length ? items.map(item => ) : }
} + {unavailable ? : !valid ? :
{items.length ? items.map(item => ) : }
}
diff --git a/apps/web/tests/evidence-card.spec.ts b/apps/web/tests/evidence-card.spec.ts index 8b82d3a..223e6af 100644 --- a/apps/web/tests/evidence-card.spec.ts +++ b/apps/web/tests/evidence-card.spec.ts @@ -11,6 +11,7 @@ test("activity cards explain freshness, sample size and coordinate precision", a await expect(first).toContainText("Наблюдения"); await expect(first).toContainText("Игроки"); await expect(first).toContainText("Координаты"); + await expect(first).toContainText("Период"); if (await first.locator('[data-passport-status="incomplete"]').count()) { await expect(first).toContainText("Минимум для рекомендации: 3 наблюдения"); } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index efdddac..f81b5c2 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 описаны первый ответ и обязательное объяснение. Осталось провести ручной task-based review и собрать completion rate на пилоте. - [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Главная уже сохраняет рыбу, водоём, период и сортировку в shareable URL и после поиска явно показывает контекст запроса, включая мобильный режим; empty-state предлагает вернуться к полному набору данных. Осталось показать 3–5 вариантов с понятным CTA и добавить режим map. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. -- [ ] **U03 · Evidence/trust card.** Общий evidence-контракт используется на activity-карточках, detail точки, водоёма и карточках снастей: freshness с текстовым `Свежо`/`Устарело`, completeness, confidence/статус, source badges и доступные доменные поля; при выборке меньше 3 явно показан минимум для рекомендации. Targeted E2E и unit-тест проверяют публичные пути и 48-часовой порог. Осталось добавить период выборки и конфликт источников. +- [ ] **U03 · Evidence/trust card.** Общий evidence-контракт используется на activity-карточках, detail точки, водоёма и карточках снастей: freshness с текстовым `Свежо`/`Устарело`, явный период расчёта, completeness, confidence/статус, source badges и доступные доменные поля; при выборке меньше 3 явно показан минимум для рекомендации. Targeted E2E и unit-тест проверяют публичные пути, период и 48-часовой порог. Осталось добавить конфликт источников. - [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Следующий шаг — единый list/map-контракт после подтверждения геометрии; на mobile карта должна открываться отдельным действием. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу. - [ ] **U05 · Mobile-first и сохранённый план рыбалки.** `/plan` поддерживает список до 5 локальных вариантов, удаление, очистку, переход к точке, print/PDF и восстановление из shareable URL; кнопка «Поделиться планом» использует native share или clipboard fallback. Detail-кнопка сохраняет данные с `aria-pressed` и восстанавливается после reload. Осталось сделать отдельную проверку print на 320/390 px и расширить сравнение полями метода/риска. - [ ] **U06 · Контентная и визуальная иерархия.** Для detail точки действие «Что взять» выделено отдельным заголовком, а provenance и качество собраны в общем паспорте данных; статусы дополнительно передаются текстом, а малая выборка не маркируется как готовая рекомендация. Осталось сократить конкурирующие цифры на карточках и провести ручной review на 5 ключевых маршрутах.