From ef8355904c818f5f082c60a5e3e24195dcafaa3b Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 7 Sep 2026 17:12:41 +0700 Subject: [PATCH] feat: paginate public field signals --- README.md | 2 +- apps/web/src/layouts/Layout.astro | 1 + apps/web/src/pages/index.astro | 12 +++++++++++- apps/web/src/styles/signal-pagination.css | 1 + docs/ROADMAP.md | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/styles/signal-pagination.css diff --git a/README.md b/README.md index 5b42e73..5ceac0f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ RF4 Spotter — неофициальный сервис свежих точек RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изолированный staging. Полные записи с ранее подтверждёнными алиасами источника публикуются автоматически; новые соответствия и неполные записи остаются на ручной проверке. Для разрешённых community-источников действует интервал не менее 30 минут на источник. Открытая альфа не использует продуктовый allowlist: интерфейс показывает весь корректно загруженный разрешённый каталог, сохраняя требования полноты и модерации. -На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. +На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Базовый SEO-контур готов для `rf4spotter.ru`: страницы имеют уникальные метаданные, canonical, Open Graph/Twitter Card, фирменное изображение 1200×630 и JSON-LD; доступны динамические `/robots.txt` и `/sitemap.xml`, административные и ошибочные страницы закрыты от индексации, добавлена собственная страница 404. Индексируемые каталоги рыб и водоёмов, detail-страницы и сочетания водоём + рыба строятся из актуального разрешённого справочника и включаются в sitemap. Подключены фирменные favicon/app icons, web manifest и production-кэширование статических ресурсов. Карточки активности показывают единый паспорт данных: источники, свежесть, полноту и уровень доверия. На `/status` опубликована легенда цветов всех источников и статусов качества. diff --git a/apps/web/src/layouts/Layout.astro b/apps/web/src/layouts/Layout.astro index 678c859..ad5aa41 100644 --- a/apps/web/src/layouts/Layout.astro +++ b/apps/web/src/layouts/Layout.astro @@ -9,6 +9,7 @@ import "../styles/activity-timeline.css"; import "../styles/fish-silhouette.css"; import "../styles/empty-states.css"; import "../styles/alpha-banner.css"; +import "../styles/signal-pagination.css"; import FishingIcon from "../components/FishingIcon.astro"; import AlphaBanner from "../components/AlphaBanner.astro"; const { diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro index b7f74b9..f7ea059 100644 --- a/apps/web/src/pages/index.astro +++ b/apps/web/src/pages/index.astro @@ -10,11 +10,17 @@ const hours = params.get("hours") ?? "24"; const waterbody = params.get("waterbody") ?? ""; const fish = params.get("fish") ?? ""; const sort = params.get("sort") ?? "activity"; +const requestedSignalLimit = Number(params.get("signals") ?? 12); +const signalLimit = Number.isInteger(requestedSignalLimit) ? Math.min(48, Math.max(12, requestedSignalLimit)) : 12; let items: Activity[] = [], signals: PublicObservation[] = [], fishes: DictionaryItem[] = [], waterbodies: DictionaryItem[] = []; +let hasMoreSignals = false; const filterError = !["6", "12", "24", "72"].includes(hours) || !["activity", "confidence", "freshness"].includes(sort); let unavailable = false; try { - [fishes, waterbodies, signals] = await Promise.all([api("/api/v1/fishes"), api("/api/v1/waterbodies"), api("/api/v1/community-observations?limit=12")]); + let signalRows: PublicObservation[] = []; + [fishes, waterbodies, signalRows] = await Promise.all([api("/api/v1/fishes"), api("/api/v1/waterbodies"), api(`/api/v1/community-observations?limit=${signalLimit + 1}`)]); + hasMoreSignals = signalRows.length > signalLimit; + signals = signalRows.slice(0, signalLimit); if (!filterError) { const query = new URLSearchParams({ hours, waterbody, fish, sort }); items = await api(`/api/v1/activity?${query}`); @@ -26,6 +32,9 @@ const selectedFish = fishes.find(item => item.slug === fish)?.name_ru ?? "Люб const periodLabel = { "6": "6 часов", "12": "12 часов", "24": "24 часа", "72": "72 часа" }[hours] ?? hours; const sortLabel = { activity: "Сначала активные", confidence: "Сначала надёжные", freshness: "Сначала свежие" }[sort] ?? sort; const filtersChanged = Boolean(waterbody || fish || hours !== "24" || sort !== "activity"); +const moreSignalParams = new URLSearchParams(params); +moreSignalParams.set("signals", String(Math.min(48, signalLimit + 12))); +const moreSignalsHref = `/?${moreSignalParams.toString()}#signals-title`; const datasetJsonLd = { "@context": "https://schema.org", "@type": "Dataset", name: "RF4 Spotter — наблюдения об активности рыбы", @@ -50,6 +59,7 @@ const datasetJsonLd = { {items[0] && leaderLevel && } {signals.length > 0 &&
Сырые данные источников

Полевые сигналы

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

{signals.length} новых
{signals.map(signal =>
{signal.quality === "incomplete" ? "!" : "?"} {signal.quality === "incomplete" ? "Неполные данные" : "Ждёт проверки"}

{signal.fish_name}

{signal.waterbody_name}

Точка
{signal.x == null || signal.y == null ? "не указана" : `${signal.x}:${signal.y}`}
Вес
{signal.weight_g == null ? "не указан" : kg(signal.weight_g)}
Получено
{ago(signal.last_seen_at)}

{signal.missing_fields.length ? "Не хватает:" : "Статус:"} {signal.missing_fields.join(", ") || "проверяется соответствие справочнику"}

)}
} + {hasMoreSignals && signalLimit < 48 && Показать ещё {signalLimit} из доступных}
Как читать данные

Не обещаем рыбу.
Показываем факты.

01

Свежесть

Чем старше сообщение, тем меньше оно влияет на активность.

02

Разные игроки

Десять уловов одного человека не равны десяти подтверждениям.

03

Уверенность

Каждая оценка объясняет, сколько данных за ней стоит.