fix: clarify blocked source status
This commit is contained in:
@@ -34,7 +34,7 @@ export type MediaAsset = { id: string; entity_type: "fish" | "waterbody" | "tack
|
||||
|
||||
export const spotPath = (item: Pick<Activity, "waterbody_slug" | "x" | "y">) => `/spots/${item.waterbody_slug}-${item.x}x${item.y}`;
|
||||
|
||||
export { activityLevel, ago, freshnessStatus, kg, plural } from "./presentation";
|
||||
export { activityLevel, ago, freshnessStatus, kg, plural, sourceStatusLabel } from "./presentation";
|
||||
|
||||
const base = process.env.API_INTERNAL_URL || import.meta.env.API_INTERNAL_URL || "http://localhost:8000";
|
||||
|
||||
|
||||
@@ -27,3 +27,11 @@ export function freshnessStatus(value: string | null | undefined, now = Date.now
|
||||
if (!Number.isFinite(timestamp)) return "unknown";
|
||||
return now - timestamp > 48 * 60 * 60 * 1000 ? "stale" : "fresh";
|
||||
}
|
||||
|
||||
export function sourceStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
healthy: "Актуален", stale: "Данные устарели", temporarily_limited: "Источник временно ограничен",
|
||||
source_changed: "Источник изменился", waiting: "Ожидает запуска", disabled: "Выключен",
|
||||
};
|
||||
return labels[status] ?? "Состояние не определено";
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import SourceBadge from "../components/SourceBadge.astro";
|
||||
import DataLegend from "../components/DataLegend.astro";
|
||||
import PageHero from "../components/PageHero.astro";
|
||||
import StatePanel from "../components/StatePanel.astro";
|
||||
import { ago, api, plural, type SourceStatus } from "../lib/api";
|
||||
import { ago, api, plural, sourceStatusLabel, type SourceStatus } from "../lib/api";
|
||||
let sources: SourceStatus[] = [], unavailable = false;
|
||||
try { sources = await api<SourceStatus[]>("/api/v1/source-status"); } catch { unavailable = true; }
|
||||
const labels = { healthy:"Актуален", stale:"Данные устарели", temporarily_limited:"Временная пауза", source_changed:"Источник изменился", waiting:"Ожидает запуска", disabled:"Выключен" };
|
||||
---
|
||||
<Layout title="Состояние источников — RF4 Spotter" description="Текущее безопасное состояние источников данных RF4 Spotter.">
|
||||
<PageHero eyebrow="Прозрачность данных" title="Статус источников" description="Пауза или изменение страницы не удаляют уже опубликованные данные." />
|
||||
{unavailable ? <StatePanel tone="unavailable" title="Статус временно недоступен" description="Данные о состоянии источников не получены. Публичные наблюдения при этом не изменяются." /> : <section class="source-health-grid content-grid">{sources.map(source => <article data-status={source.status}><div><SourceBadge source={source.source_system}/><span class="health-state"><i></i>{labels[source.status]}</span></div><h2>{source.name}</h2><strong>{source.observations} {plural(source.observations,["наблюдение","наблюдения","наблюдений"])}</strong><p>{source.last_success_at ? `Последнее успешное обновление ${ago(source.last_success_at)}` : "Успешных запусков пока нет"}</p></article>)}</section>}
|
||||
{unavailable ? <StatePanel tone="unavailable" title="Статус временно недоступен" description="Данные о состоянии источников не получены. Публичные наблюдения при этом не изменяются." /> : <section class="source-health-grid content-grid">{sources.map(source => <article data-status={source.status}><div><SourceBadge source={source.source_system}/><span class="health-state"><i></i>{sourceStatusLabel(source.status)}</span></div><h2>{source.name}</h2><strong>{source.observations} {plural(source.observations,["наблюдение","наблюдения","наблюдений"])}</strong><p>{source.status === "temporarily_limited" ? "Источник ограничен; опубликованные данные сохранены." : source.last_success_at ? `Последнее успешное обновление ${ago(source.last_success_at)}` : "Успешных запусков пока нет"}</p></article>)}</section>}
|
||||
<DataLegend />
|
||||
</Layout>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { activityLevel, freshnessStatus, plural } from "../../src/lib/presentation.ts";
|
||||
import { activityLevel, freshnessStatus, plural, sourceStatusLabel } from "../../src/lib/presentation.ts";
|
||||
import { fishVisualFamily } from "../../src/lib/fish-visuals.ts";
|
||||
import { tackleVisualKind, tackleVisualTone } from "../../src/lib/tackle-visuals.ts";
|
||||
import { waterbodyVisual } from "../../src/lib/waterbody-visuals.ts";
|
||||
@@ -30,6 +30,12 @@ test("freshness status is explicit and deterministic around the 48-hour boundary
|
||||
assert.equal(freshnessStatus(null, now), "unknown");
|
||||
});
|
||||
|
||||
test("source status makes a blocked provider explicit", () => {
|
||||
assert.equal(sourceStatusLabel("temporarily_limited"), "Источник временно ограничен");
|
||||
assert.equal(sourceStatusLabel("healthy"), "Актуален");
|
||||
assert.equal(sourceStatusLabel("unexpected"), "Состояние не определено");
|
||||
});
|
||||
|
||||
test("fish visual families follow body type instead of a name hash", () => {
|
||||
assert.deepEqual(
|
||||
["Щука", "Озёрная форель", "Карп", "Окунь", "Сом", "Угорь", "Палтус", "Треска", "Неизвестная рыба"].map(fishVisualFamily),
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@
|
||||
- [ ] **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/mobile-контракт проверяет 320/390 px, лимит импорта и отсутствие горизонтального overflow; карточка плана теперь сравнивает подтверждённые метод/проводку и явно показывает риск по выборке/уверенности.
|
||||
- [ ] **U06 · Контентная и визуальная иерархия.** Для detail точки действие «Что взять» выделено отдельным заголовком, а provenance и качество собраны в общем паспорте данных; статусы дополнительно передаются текстом, малая выборка не маркируется как готовая рекомендация, а дублирующие catches/players/confidence убраны из вторичной колонки activity-карточки. Осталось провести ручной review на 5 ключевых маршрутах.
|
||||
- [ ] **U07 · UX-приёмка и измерения.** Playwright-контракты уже покрывают query journey, evidence states, saved plan, share/print, accessibility и visual matrix; unit-контракт добавил детерминированный stale-порог, а insufficient-data теперь отделён от incomplete в публичном паспорте. Их критерии собраны в [ux-contract.md](ux-contract.md). Осталось добавить отдельный blocked fixture, сохранить reference screenshots и получить production Lighthouse/CLS/INP/time-to-first-useful-answer после пилота.
|
||||
- [ ] **U07 · UX-приёмка и измерения.** Playwright-контракты уже покрывают query journey, evidence states, saved plan, share/print, accessibility и visual matrix; unit-контракт добавил детерминированные stale- и blocked-подписи, а insufficient-data теперь отделён от incomplete в публичном паспорте. `/status` явно сообщает, что при ограничении источника опубликованные данные сохранены. Осталось сохранить reference screenshots и получить production Lighthouse/CLS/INP/time-to-first-useful-answer после пилота.
|
||||
|
||||
- [ ] **Q01 · Документы источников — реестр готов, нужны первичные подтверждения.** Создан единый production-gate с атрибуцией, общим лимитом 30 минут, хранением и процедурой отзыва для RF4DB, RF4-STAT, RF4MAP, RF4 Posts и официального RF4. До открытой публикации приложить устойчивые ссылки/копии первичных разрешений, контакты, даты и отдельно подтвердить право на изображения; пустое поле блокирует соответствующий источник.
|
||||
- [x] **Q02 · Управляемое удаление источника.** Изменение ранее опубликованной записи возвращает её в staging, сбрасывает сопоставление и переводит связанный улов в pending с новой версией решения. Результат проверки хранится как `available`, `missing`, `temporary_error` или `blocked`: только подтверждённый `missing` отзывает публикацию, временная ошибка и блокировка остаются диагностикой. Повторное появление требует ручного подтверждения. Withdrawn-записи исключены из публичной активности, а статус и время проверки доступны в admin provenance и журнале решений. Отдельного сетевого обхода нет: Q03 подключит эту реакцию к разрешённому плановому запросу.
|
||||
|
||||
Reference in New Issue
Block a user