feat: add spot coordinate radar

This commit is contained in:
ik
2026-09-07 17:04:59 +07:00
parent ebdeaf3659
commit 72057c7b99
6 changed files with 20 additions and 3 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изо
Базовый 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` опубликована легенда цветов всех источников и статусов качества. Базовый 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` опубликована легенда цветов всех источников и статусов качества.
Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. На странице точки координаты дополнительно показаны фирменным радаром, который не имитирует отсутствующую географию водоёма.
Все пять community-парсеров подключены к отдельному scheduler-процессу. Состояние запусков и ошибок хранится в PostgreSQL, параллельный запуск одного источника блокируется, минимальный интервал жёстко ограничен 1800 секундами. Локально процесс включается профилем `docker compose --profile scheduler up -d`; detail-URL RF4MAP/RF4 Posts задаются переменными окружения. Все пять community-парсеров подключены к отдельному scheduler-процессу. Состояние запусков и ошибок хранится в PostgreSQL, параллельный запуск одного источника блокируется, минимальный интервал жёстко ограничен 1800 секундами. Локально процесс включается профилем `docker compose --profile scheduler up -d`; detail-URL RF4MAP/RF4 Posts задаются переменными окружения.
@@ -0,0 +1,14 @@
---
const { x, y } = Astro.props as { x: number; y: number };
---
<div class="coordinate-radar" role="img" aria-label={`Координатный радар точки X ${x}, Y ${y}`}>
<svg viewBox="0 0 180 180" aria-hidden="true">
<circle cx="90" cy="90" r="72"/><circle cx="90" cy="90" r="48"/><circle cx="90" cy="90" r="24"/>
<path d="M90 10v160M10 90h160M34 34l112 112M146 34 34 146"/>
<path class="radar-sweep" d="M90 90 90 18A72 72 0 0 1 157 64Z"/>
<circle class="radar-mark" cx="90" cy="90" r="7"/><circle class="radar-pulse" cx="90" cy="90" r="13"/>
</svg>
<span class="radar-north">N</span>
<strong><small>X</small>{x}<i></i><small>Y</small>{y}</strong>
<span class="radar-caption">Координаты в игре</span>
</div>
+1
View File
@@ -4,6 +4,7 @@ import "../styles/catalog.css";
import "../styles/source-status.css"; import "../styles/source-status.css";
import "../styles/data-passport.css"; import "../styles/data-passport.css";
import "../styles/data-legend.css"; import "../styles/data-legend.css";
import "../styles/coordinate-radar.css";
import FishingIcon from "../components/FishingIcon.astro"; import FishingIcon from "../components/FishingIcon.astro";
const { const {
title = "RF4 Spotter — свежие точки клёва Russian Fishing 4", title = "RF4 Spotter — свежие точки клёва Russian Fishing 4",
+2 -1
View File
@@ -1,6 +1,7 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import SourceBadge from "../../components/SourceBadge.astro"; import SourceBadge from "../../components/SourceBadge.astro";
import CoordinateRadar from "../../components/CoordinateRadar.astro";
import { activityLevel, api, kg, plural, type Activity, type Catch, type Spot } from "../../lib/api"; import { activityLevel, api, kg, plural, type Activity, type Catch, type Spot } from "../../lib/api";
const { id } = Astro.params; const { id } = Astro.params;
let spot: Spot | null = null, catches: Catch[] = [], activity: Activity | null = null, unavailable = false; let spot: Spot | null = null, catches: Catch[] = [], activity: Activity | null = null, unavailable = false;
@@ -23,7 +24,7 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
<Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs}> <Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs}>
<a class="back" href="/">← Все активные точки</a> <a class="back" href="/">← Все активные точки</a>
{unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <> {unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <>
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p></div><div class="pin">{spot.x}<span>:</span>{spot.y}</div></section> <section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
<div class="periods"><div><strong>{spot.catches_24h}</strong><span>за 24 часа</span></div><div><strong>{spot.catches_3d}</strong><span>за 3 дня</span></div><div><strong>{spot.catches_7d}</strong><span>за 7 дней</span></div></div> <div class="periods"><div><strong>{spot.catches_24h}</strong><span>за 24 часа</span></div><div><strong>{spot.catches_3d}</strong><span>за 3 дня</span></div><div><strong>{spot.catches_7d}</strong><span>за 7 дней</span></div></div>
<section class="detail-grid"><div><div class="section-heading"><h2>Последние уловы</h2></div>{catches.length ? <div class="catch-list">{catches.map(item => <article><div><strong>{item.fish}</strong><span>{item.bait ?? "Приманка не указана"}</span><SourceBadge source={item.source_system} href={item.source_url}/></div><div><strong>{kg(item.weight_g)}</strong><span>{item.player_name ?? "Анонимно"}</span></div></article>)}</div> : <div class="state compact-state"><h2>Уловов пока нет</h2><p>Для этой точки нет одобренных наблюдений.</p></div>}</div><aside><span class="eyebrow">Оценка за 24 часа</span>{activity && level ? <><h2>{activity.activity_score} / 100</h2><strong data-activity-level={level.short}>{level.description}</strong><div class="source-strip">{activity.sources.map(source => <SourceBadge source={source}/>)}</div><p>{activity.explanation}</p><p class="note">Основано на {activity.catches} {plural(activity.catches, ["наблюдении", "наблюдениях", "наблюдениях"])} от {activity.unique_players} {plural(activity.unique_players, ["игрока", "игроков", "игроков"])}. Уверенность: {activity.confidence_score}%.</p></> : <p>За последние 24 часа данных для расчёта нет.</p>}<span class="eyebrow">Лучшие приманки</span>{spot.top_baits.length ? <ol>{spot.top_baits.map(name => <li>{name}</li>)}</ol> : <p>Недостаточно данных.</p>}<p class="note">Учитываются только одобренные наблюдения.</p></aside></section> <section class="detail-grid"><div><div class="section-heading"><h2>Последние уловы</h2></div>{catches.length ? <div class="catch-list">{catches.map(item => <article><div><strong>{item.fish}</strong><span>{item.bait ?? "Приманка не указана"}</span><SourceBadge source={item.source_system} href={item.source_url}/></div><div><strong>{kg(item.weight_g)}</strong><span>{item.player_name ?? "Анонимно"}</span></div></article>)}</div> : <div class="state compact-state"><h2>Уловов пока нет</h2><p>Для этой точки нет одобренных наблюдений.</p></div>}</div><aside><span class="eyebrow">Оценка за 24 часа</span>{activity && level ? <><h2>{activity.activity_score} / 100</h2><strong data-activity-level={level.short}>{level.description}</strong><div class="source-strip">{activity.sources.map(source => <SourceBadge source={source}/>)}</div><p>{activity.explanation}</p><p class="note">Основано на {activity.catches} {plural(activity.catches, ["наблюдении", "наблюдениях", "наблюдениях"])} от {activity.unique_players} {plural(activity.unique_players, ["игрока", "игроков", "игроков"])}. Уверенность: {activity.confidence_score}%.</p></> : <p>За последние 24 часа данных для расчёта нет.</p>}<span class="eyebrow">Лучшие приманки</span>{spot.top_baits.length ? <ol>{spot.top_baits.map(name => <li>{name}</li>)}</ol> : <p>Недостаточно данных.</p>}<p class="note">Учитываются только одобренные наблюдения.</p></aside></section>
</>} </>}
+1
View File
@@ -0,0 +1 @@
.coordinate-radar{position:relative;flex:0 0 180px;width:180px;height:180px;display:grid;place-items:center;color:var(--lime)}.coordinate-radar svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:#ffffff20;stroke-width:1}.coordinate-radar .radar-sweep{fill:#c9f45b0d;stroke:#c9f45b38}.coordinate-radar .radar-mark{fill:var(--lime);stroke:none}.coordinate-radar .radar-pulse{stroke:var(--lime);stroke-width:2;opacity:.55}.coordinate-radar strong{position:relative;display:grid;grid-template-columns:auto auto 1px auto auto;align-items:baseline;gap:4px;padding:7px 10px;border-radius:7px;background:#082226d9;font:400 20px Georgia,serif}.coordinate-radar strong small{font:700 8px Inter,sans-serif;color:#9eb0ad}.coordinate-radar strong i{width:1px;height:14px;background:#ffffff35}.radar-north{position:absolute;top:3px;font-size:8px;letter-spacing:.15em}.radar-caption{position:absolute;bottom:1px;color:#91a5a1;font-size:8px;text-transform:uppercase;letter-spacing:.12em}@media(max-width:720px){.coordinate-radar{display:none}}
+1 -1
View File
@@ -124,7 +124,7 @@
### Графические элементы и объяснение данных ### Графические элементы и объяснение данных
- [x] Подготовить фирменный знак «крючок + поплавок + координатная сетка» и согласованный набор favicon/app icons (7 сентября 2026). - [x] Подготовить фирменный знак «крючок + поплавок + координатная сетка» и согласованный набор favicon/app icons (7 сентября 2026).
- [ ] Добавить координатный радар или мини-карту на страницу точки. - [x] Добавить честный координатный радар без имитации отсутствующей карты на страницу точки (7 сентября 2026).
- [ ] Добавить временную шкалу активности 24/72 часа в рыболовной стилистике. - [ ] Добавить временную шкалу активности 24/72 часа в рыболовной стилистике.
- [ ] Добавить лёгкие силуэты видов рыб, не перегружая карточки результатов. - [ ] Добавить лёгкие силуэты видов рыб, не перегружая карточки результатов.
- [x] Собрать единый визуальный «паспорт данных»: источник, свежесть, полнота и доверие; применить к карточкам активности (7 сентября 2026). - [x] Собрать единый визуальный «паспорт данных»: источник, свежесть, полнота и доверие; применить к карточкам активности (7 сентября 2026).