Паспорт данных{statusLabels[status]}
{sources.map(source => )}
- - Свежесть
- {ago(observedAt)}
- Полнота
- {completenessLabel}
- Доверие
- {confidence == null ? "После проверки" : `${confidence}%`}
{sampleSize != null && - Наблюдения
- {sampleSize}
}{independentPlayers != null && - Игроки
- {independentPlayers}
}{coordinatePrecision && - Координаты
- {precisionLabels[coordinatePrecision]}
}
+ - Свежесть
- {observedAt ? ago(observedAt) : "Не указана"}
- Полнота
- {completenessLabel}
- Доверие
- {confidence == null ? "После проверки" : `${confidence}%`}
{sampleSize != null && - Наблюдения
- {sampleSize}
}{independentPlayers != null && - Игроки
- {independentPlayers}
}{coordinatePrecision && - Координаты
- {precisionLabels[coordinatePrecision]}
}
diff --git a/apps/web/src/pages/tackle/index.astro b/apps/web/src/pages/tackle/index.astro
index 8efa92b..e1757f0 100644
--- a/apps/web/src/pages/tackle/index.astro
+++ b/apps/web/src/pages/tackle/index.astro
@@ -4,6 +4,7 @@ import PageHero from "../../components/PageHero.astro";
import StatePanel from "../../components/StatePanel.astro";
import Pagination from "../../components/Pagination.astro";
import TackleGlyph from "../../components/TackleGlyph.astro";
+import DataPassport from "../../components/DataPassport.astro";
import { api, type PaginatedTackleItems } from "../../lib/api";
const params = Astro.url.searchParams;
@@ -35,6 +36,6 @@ if (family) filterParams.set("family", family);
Карта и образ водоёма{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 && {items.length ? items.map(item =>
) : } }
diff --git a/apps/web/src/styles/query-context.css b/apps/web/src/styles/query-context.css
index 6163905..e3fec2e 100644
--- a/apps/web/src/styles/query-context.css
+++ b/apps/web/src/styles/query-context.css
@@ -5,4 +5,5 @@
.query-summary__filters span{padding:6px 10px;border:1px solid #c8d3cb;border-radius:20px;background:#f8faf5;color:#526662;font-size:12px}
.query-summary__filters a{margin-left:4px;color:#4d625e;font-size:13px;text-underline-offset:3px}
.detail-action-heading{margin:27px 0 8px;font:400 24px Georgia,serif;color:var(--lime)}
+.tackle-card-passport{grid-column:1/-1;min-width:0}.tackle-card-passport .data-passport{margin-top:14px}
@media(max-width:720px){.query-summary{display:block;padding:16px 14px 0;width:100%}.query-summary h2{font-size:23px}.query-summary__filters{justify-content:flex-start;flex-wrap:nowrap;overflow-x:auto;padding-top:11px;scroll-snap-type:x proximity;scrollbar-width:none}.query-summary__filters::-webkit-scrollbar{display:none}.query-summary__filters span{flex:0 0 auto;scroll-snap-align:start}.query-summary__filters a{position:sticky;right:0;flex:0 0 auto;padding:6px 10px;background:var(--paper)}}
diff --git a/apps/web/tests/catalog-evidence.spec.ts b/apps/web/tests/catalog-evidence.spec.ts
new file mode 100644
index 0000000..6bd9b9b
--- /dev/null
+++ b/apps/web/tests/catalog-evidence.spec.ts
@@ -0,0 +1,12 @@
+import { expect, test } from "@playwright/test";
+
+test("waterbody passport and honest empty tackle catalog are explicit", async ({ page }) => {
+ await page.goto("/waterbodies/р-вьюнок");
+ const waterPassport = page.locator('.data-passport[aria-label="Паспорт данных"]');
+ await expect(waterPassport).toBeVisible();
+ await expect(waterPassport.getByText("Паспорт данных")).toBeVisible();
+ await expect(waterPassport.getByText("Свежесть")).toBeVisible();
+
+ await page.goto("/tackle");
+ await expect(page.getByText("Подтверждённых карточек пока нет")).toBeVisible();
+});
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index cebb853..8436970 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -94,7 +94,7 @@
- [ ] **U01 · UX-контракт и scorecard.** Зафиксировать task-based навигацию, смысловой порядок ответа, словарь статусов (`fresh`, `stale`, `insufficient_data`, `blocked`, `temporary_error`) и единый scorecard: time-to-first-useful-answer, filter-to-result steps, completion rate, overflow, axe и keyboard regression. Сверить каждый публичный маршрут с контрактом; не добавлять фичи, которые не улучшают решение игрока.
- [ ] **U02 · Главный сценарий «рыба → водоём → точка → снасть».** Главная уже сохраняет рыбу, водоём, период и сортировку в shareable URL и после поиска явно показывает контекст запроса, включая мобильный режим; empty-state предлагает вернуться к полному набору данных. Осталось показать 3–5 вариантов с понятным CTA и добавить режим map. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры.
-- [ ] **U03 · Evidence/trust card.** Общий evidence-контракт теперь используется и на activity-карточках, и на detail точки: freshness, confidence, число наблюдений, независимые игроки, `coordinate_precision`, source badges и явный статус `Неполные данные` при малой выборке; targeted E2E проверяет оба пути. Осталось протянуть его на waterbody и tackle, добавить период выборки, completeness, конфликт источников и правило минимальной выборки для любых рекомендаций. Нельзя показывать «лучшую точку» или AI-like рекомендацию без объяснения и минимальной выборки.
+- [ ] **U03 · Evidence/trust card.** Общий evidence-контракт используется на activity-карточках, detail точки, водоёма и карточках снастей: freshness, completeness, confidence/статус, source badges и доступные доменные поля; targeted E2E проверяет публичные пути. Осталось добавить период выборки, конфликт источников и правило минимальной выборки для любых рекомендаций. Нельзя показывать «лучшую точку» или AI-like рекомендацию без объяснения и минимальной выборки.
- [ ] **U04 · List/map и progressive disclosure.** List остаётся честным базовым режимом: фильтры, сортировка, URL-состояние и evidence-карточки уже работают без имитации координатной карты. Следующий шаг — единый list/map-контракт после подтверждения геометрии; на mobile карта должна открываться отдельным действием. Вторичные raw/provenance-поля не исчезают и раскрываются по запросу.
- [ ] **U05 · Mobile-first и сохранённый план рыбалки.** Добавить лёгкий сценарий сравнения и сохранения 3–5 вариантов без обязательного аккаунта: рыба, водоём, координаты, метод, наживка, свежесть, доверие и риск. Acceptance: shareable URL/print-карточка, 320/390 px, восстановление после reload, удаление локального плана без внешнего сервиса.
- [ ] **U06 · Контентная и визуальная иерархия.** Для detail точки действие «Что взять» выделено отдельным заголовком, а provenance и качество собраны в общем паспорте данных; статусы дополнительно передаются текстом, не только цветом. Осталось сократить конкурирующие цифры на карточках и провести ручной review на 5 ключевых маршрутах.