From d1a5ad1b224f4975cb410e21583e28a2b4fe960d Mon Sep 17 00:00:00 2001 From: IK Date: Sun, 20 Sep 2026 19:50:19 +0700 Subject: [PATCH] feat: add activity evidence card --- apps/web/src/components/ActivityCard.astro | 2 +- apps/web/src/components/DataPassport.astro | 7 ++++--- apps/web/src/styles/data-passport.css | 4 ++-- apps/web/tests/evidence-card.spec.ts | 14 ++++++++++++++ docs/ROADMAP.md | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 apps/web/tests/evidence-card.spec.ts diff --git a/apps/web/src/components/ActivityCard.astro b/apps/web/src/components/ActivityCard.astro index 51147f0..80f7e75 100644 --- a/apps/web/src/components/ActivityCard.astro +++ b/apps/web/src/components/ActivityCard.astro @@ -18,7 +18,7 @@ const precision = { exact: "точные", approximate: "приблизител
{item.x}:{item.y} · {precision} {ago(item.last_confirmed_at)}

{item.explanation}

- +
Работает сейчас{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 192803f..07a550a 100644 --- a/apps/web/src/components/DataPassport.astro +++ b/apps/web/src/components/DataPassport.astro @@ -1,13 +1,14 @@ --- import SourceBadge from "./SourceBadge.astro"; import { ago } from "../lib/api"; -type Props = { sources: string[]; sourceUrl?: string | null; observedAt: string; completeness?: number | null; confidence?: number | null; status?: "verified" | "unverified" | "incomplete" }; -const { sources, sourceUrl, observedAt, completeness = null, confidence = null, status = "verified" } = Astro.props; +type Props = { sources: string[]; sourceUrl?: string | null; observedAt: string; 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; const statusLabels = { verified: "Учтено", unverified: "Ждёт проверки", incomplete: "Неполные данные" }; const completenessLabel = completeness == null ? "Не рассчитана" : `${Math.min(100, Math.max(0, completeness))}% полей`; +const precisionLabels = { exact: "точные", approximate: "приблизительные", area: "район", missing: "не указаны" }; ---
Паспорт данных{statusLabels[status]}
{sources.map(source => )}
-
Свежесть
{ago(observedAt)}
Полнота
{completenessLabel}
Доверие
{confidence == null ? "После проверки" : `${confidence}%`}
+
Свежесть
{ago(observedAt)}
Полнота
{completenessLabel}
Доверие
{confidence == null ? "После проверки" : `${confidence}%`}
{sampleSize != null &&
Наблюдения
{sampleSize}
}{independentPlayers != null &&
Игроки
{independentPlayers}
}{coordinatePrecision &&
Координаты
{precisionLabels[coordinatePrecision]}
}
diff --git a/apps/web/src/styles/data-passport.css b/apps/web/src/styles/data-passport.css index 77fb9bc..e5f3421 100644 --- a/apps/web/src/styles/data-passport.css +++ b/apps/web/src/styles/data-passport.css @@ -1,5 +1,5 @@ .data-passport{margin-top:18px;padding:14px;border:1px solid var(--border);border-radius:13px;background:var(--surface-soft);color:var(--deep)} .data-passport header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}.data-passport header>span{font:italic 15px Georgia,serif}.data-passport header strong{padding:4px 8px;border-radius:999px;background:var(--success-soft);color:var(--success);font-size:9px;text-transform:uppercase;letter-spacing:.06em}.data-passport header strong[data-passport-status="unverified"]{background:var(--info-soft);color:var(--info)}.data-passport header strong[data-passport-status="incomplete"]{background:var(--warning-soft);color:var(--warning)} -.data-passport__sources{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}.data-passport dl{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:0}.data-passport dl div{padding-top:8px;border-top:1px solid var(--border-soft)}.data-passport dt{font-size:8px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-subtle)}.data-passport dd{margin:3px 0 0;font:400 12px Georgia,serif;color:var(--deep)} +.data-passport__sources{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:12px}.data-passport dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:0}.data-passport dl div{padding-top:8px;border-top:1px solid var(--border-soft)}.data-passport dt{font-size:8px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-subtle)}.data-passport dd{margin:3px 0 0;font:400 12px Georgia,serif;color:var(--deep)} .detail-card .data-passport,.detail-grid aside .data-passport{border-color:#ffffff20;background:#ffffff08;color:#fff}.detail-card .data-passport dd,.detail-grid aside .data-passport dd{color:#fff}.detail-card .data-passport dl div,.detail-grid aside .data-passport dl div{border-color:#ffffff1c}.detail-card .data-passport dt,.detail-grid aside .data-passport dt{color:#a9b8b5}.signal-card .data-passport{position:relative;z-index:1;margin-top:15px;background:#fffdf7;border-color:#ddd3af} -@media(max-width:420px){.data-passport dl{grid-template-columns:1fr}.data-passport dl div{display:flex;justify-content:space-between;gap:10px}} +@media(max-width:720px){.data-passport dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:420px){.data-passport dl{grid-template-columns:1fr}.data-passport dl div{display:flex;justify-content:space-between;gap:10px}} diff --git a/apps/web/tests/evidence-card.spec.ts b/apps/web/tests/evidence-card.spec.ts new file mode 100644 index 0000000..976053c --- /dev/null +++ b/apps/web/tests/evidence-card.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from "@playwright/test"; + +test("activity cards explain freshness, sample size and coordinate precision", async ({ page }) => { + await page.goto("/"); + + const passport = page.locator(".spot-card .data-passport"); + const count = await passport.count(); + expect(count).toBeGreaterThan(0); + const first = passport.first(); + await expect(first).toContainText("Свежесть"); + await expect(first).toContainText("Наблюдения"); + await expect(first).toContainText("Игроки"); + await expect(first).toContainText("Координаты"); +}); diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index dcc38f1..f309f66 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 · Главный сценарий «рыба → водоём → точка → снасть».** Перестроить home/filter journey так, чтобы один URL сохранял рыбу, водоём, период, сортировку и режим list/map; показать 3–5 вариантов с понятным CTA и сохранить текущие empty/error/partial states. Acceptance: первый полезный вариант виден без регистрации, back/refresh сохраняют контекст, mobile не теряет фильтры. -- [ ] **U03 · Evidence/trust card.** Ввести общий компонент доказательности для activity, spot, waterbody и tackle: freshness, период, число наблюдений, независимые игроки, `coordinate_precision`, completeness, source badges, confidence и конфликт источников. Нельзя показывать «лучшую точку» или AI-like рекомендацию без объяснения и минимальной выборки. +- [ ] **U03 · Evidence/trust card.** Для activity-карточек добавлен первый общий evidence-контракт: freshness, confidence, число наблюдений, независимые игроки, `coordinate_precision`, source badges и явный статус `Неполные данные` при малой выборке; targeted E2E проверяет эти поля. Осталось протянуть тот же контракт на spot, waterbody и tackle, добавить период выборки, completeness, конфликт источников и правило минимальной выборки для любых рекомендаций. Нельзя показывать «лучшую точку» или AI-like рекомендацию без объяснения и минимальной выборки. - [ ] **U04 · List/map и progressive disclosure.** Добавить единый контракт list/map: одинаковые фильтры, сортировка, URL-состояние и карточки; на mobile список остаётся базовым режимом, карта открывается отдельным действием. Не перегружать первый экран: вторичные raw/provenance-поля раскрываются по запросу, но не исчезают. - [ ] **U05 · Mobile-first и сохранённый план рыбалки.** Добавить лёгкий сценарий сравнения и сохранения 3–5 вариантов без обязательного аккаунта: рыба, водоём, координаты, метод, наживка, свежесть, доверие и риск. Acceptance: shareable URL/print-карточка, 320/390 px, восстановление после reload, удаление локального плана без внешнего сервиса. - [ ] **U06 · Контентная и визуальная иерархия.** Сократить конкурирующие цифры на карточках, привести badges/source labels к одной семантике, сделать «что делать» главным текстом, а provenance — вторичным раскрываемым слоем. Провести ручной review на 5 ключевых маршрутах и проверить, что цвет не является единственным носителем статуса.