Compare commits

..
2 Commits
Author SHA1 Message Date
ik b71e4a22a2 docs: plan entity visual identification
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s
2026-09-12 15:23:45 +07:00
ik 966000b0b0 feat: transform catalogs into field atlas 2026-09-12 15:20:26 +07:00
18 changed files with 88 additions and 17 deletions
+3 -3
View File
@@ -24,7 +24,7 @@ Web Docker-образ устанавливает зависимости чере
Предыдущие аудиты и план восстановления доступны в `docs/` как исторические материалы. Их незакрытые на момент составления чекбоксы не являются текущим backlog; статусы сведены в [итоговый отчёт](docs/RECOVERY_FIXES_REPORT.md). Предыдущие аудиты и план восстановления доступны в `docs/` как исторические материалы. Их незакрытые на момент составления чекбоксы не являются текущим backlog; статусы сведены в [итоговый отчёт](docs/RECOVERY_FIXES_REPORT.md).
На ширинах 320, 390, 768 и 1280 px ранее проверено отсутствие горизонтального переполнения основных страниц. Это не полная визуальная приёмка: аудит обнаружил неверную desktop-компоновку фильтров; наполненные карточки, длинные названия, клавиатура и zoom остаются отдельной задачей. На ширинах 320, 390, 768 и 1280 px проверено отсутствие горизонтального переполнения основных страниц; исправлена desktop-компоновка фильтров. Наполненные карточки, длинные названия, клавиатура и zoom остаются постоянной частью визуальной приёмки каждого крупного UI-пакета.
Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. На всех страницах подключён компактный баннер открытой альфы со ссылками на статус, правила и отправку улова. В production Compose включён community scheduler; локально он запускается отдельным профилем. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений; публичный адрес обратной связи ещё не задан. Функциональный MVP и локальный production-контур готовятся к открытой альфе: официальный импорт, пользовательские заявки, модерация, объяснимый индекс, staging внешних источников, адаптивный Astro UI, миграции, резервное копирование, retention, мониторинг и security/accessibility-проверки реализованы. На всех страницах подключён компактный баннер открытой альфы со ссылками на статус, правила и отправку улова. В production Compose включён community scheduler; локально он запускается отдельным профилем. Публичный запуск блокируют покупка и настройка сервера, DNS/TLS, реальные секреты, внешний backup, канал уведомлений; публичный адрес обратной связи ещё не задан.
@@ -32,9 +32,9 @@ RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изо
На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Визуально объединяются только повторы одного ID источника; похожие записи разных площадок остаются самостоятельными наблюдениями. Sidebar лидера скрывается при единственном результате, чтобы не повторять ту же карточку. На сайте у каждой записи отображается источник, а у агрегированной активности — все вошедшие в расчёт источники. Неполные community-наблюдения публикуются сразу в отдельной ленте «Полевые сигналы» с предупреждением и перечнем отсутствующих полей; до подтверждения полноты они не влияют на индекс клёва. Лента раскрывается серверной кнопкой «Показать ещё», сохраняет выбранные фильтры и ограничена 48 сигналами на страницу. Визуально объединяются только повторы одного ID источника; похожие записи разных площадок остаются самостоятельными наблюдениями. Sidebar лидера скрывается при единственном результате, чтобы не повторять ту же карточку.
Базовый 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 из SVG-мастера, отдельные полнофоновые maskable-иконки, web manifest и production-кэширование статических ресурсов. Карточки активности показывают единый паспорт данных: источники, свежесть, полноту и уровень доверия. На `/status` опубликована легенда цветов всех источников и статусов качества.
Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. На странице точки координаты дополнительно показаны фирменным радаром, который не имитирует отсутствующую географию водоёма, а уловы за 72 часа — шкалой-леской с 12-часовым шагом. Каждый улов показывает источник, относительную свежесть и точное время UTC; время получения явно отделено от времени улова. Карточки активности и каталог дополнены лёгкими SVG-силуэтами рыб без внешних графических зависимостей. Пустые и аварийные состояния используют собственную CSS-иллюстрацию поплавка; анимация учитывает системное ограничение движения. Публичные точки используют постоянные читаемые адреса вида `/spots/kuori-85x92`; старые UUID-адреса остаются совместимыми и перенаправляются на канонический URL. На странице точки координаты дополнительно показаны фирменным радаром, который не имитирует отсутствующую географию водоёма, а уловы за 72 часа — шкалой-леской с 12-часовым шагом. Каждый улов показывает источник, относительную свежесть и точное время UTC; время получения явно отделено от времени улова. Каталоги оформлены как полевой атлас: тёмный seal показывает объём справочника, карточки рыб используют SVG-силуэты, карточки водоёмов — абстрактные береговые контуры и точку наблюдения. Пустые состояния используют статичную CSS-иллюстрацию поплавка; смысловые анимации полностью учитывают системное ограничение движения.
Все пять community-парсеров подключены к отдельному scheduler-процессу. Попытка резервируется в PostgreSQL до HTTP-запроса, поэтому ошибки тоже расходуют cooldown. Блокировка и минимальный интервал 1800 секунд действуют на весь домен; endpoint одного сайта выбираются по самому давнему запуску и не голодают. Ручной production-запуск использует тот же журнал: `docker compose exec api python -m app.cli fetch-community rf4stat-fishing`. Локально scheduler включается профилем `docker compose --profile scheduler up -d`; detail-URL RF4MAP/RF4 Posts задаются переменными окружения. Все пять community-парсеров подключены к отдельному scheduler-процессу. Попытка резервируется в PostgreSQL до HTTP-запроса, поэтому ошибки тоже расходуют cooldown. Блокировка и минимальный интервал 1800 секунд действуют на весь домен; endpoint одного сайта выбираются по самому давнему запуску и не голодают. Ручной production-запуск использует тот же журнал: `docker compose exec api python -m app.cli fetch-community rf4stat-fishing`. Локально scheduler включается профилем `docker compose --profile scheduler up -d`; detail-URL RF4MAP/RF4 Posts задаются переменными окружения.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" fill="#082226"/>
<g stroke="#295158" stroke-width="1" opacity=".7">
<path d="M16 8v48M32 8v48M48 8v48M8 16h48M8 32h48M8 48h48"/>
</g>
<path d="M11 46c8-5 14 5 22 0s14 5 21 0" fill="none" stroke="#6fc3c4" stroke-width="2.5" stroke-linecap="round"/>
<path d="M37 8v21" stroke="#f4efe2" stroke-width="3" stroke-linecap="round"/>
<path d="M32 17h10l-2 12h-6Z" fill="#c9f45b"/>
<path d="M37 29v13c0 9-13 11-15 2-1-5 3-8 7-7" fill="none" stroke="#c9f45b" stroke-width="4" stroke-linecap="round"/>
<circle cx="37" cy="13" r="2.5" fill="#ff785a"/>
</svg>

After

Width:  |  Height:  |  Size: 657 B

+4 -2
View File
@@ -9,7 +9,9 @@
"background_color": "#f2f5ee", "background_color": "#f2f5ee",
"theme_color": "#082226", "theme_color": "#082226",
"icons": [ "icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
] ]
} }
+6 -5
View File
@@ -1,8 +1,9 @@
--- ---
const { eyebrow, title, description } = Astro.props as { eyebrow: string; title: string; description?: string }; import FishingIcon from "./FishingIcon.astro";
import FishSilhouette from "./FishSilhouette.astro";
const { eyebrow, title, description, variant, count } = Astro.props as { eyebrow: string; title: string; description?: string; variant?: "water" | "fish"; count?: number };
--- ---
<section class="catalog-hero content-grid"> <section class:list={["catalog-hero content-grid", { "catalog-hero--illustrated": variant }]}>
<span class="overline">{eyebrow}</span> <div class="catalog-hero__copy"><span class="overline">{eyebrow}</span><h1>{title}</h1>{description && <p>{description}</p>}</div>
<h1>{title}</h1> {variant && <div class="catalog-hero__seal" aria-hidden="true"><span>{variant === "water" ? <FishingIcon name="ripple" size={54} /> : <FishSilhouette name={title} size={92} />}</span>{typeof count === "number" && <><strong>{String(count).padStart(2,"0")}</strong><small>{variant === "water" ? "водоёмов" : "видов рыб"}</small></>}</div>}
{description && <p>{description}</p>}
</section> </section>
@@ -0,0 +1,15 @@
---
const { name, size = 112 } = Astro.props as { name: string; size?: number };
const variant = [...name].reduce((sum, char) => sum + (char.codePointAt(0) ?? 0), 0) % 3;
const shores = [
"M8 57C22 42 34 48 47 34S75 20 91 34s16 7 25-3",
"M7 48c17-20 34 5 50-10s28-15 38 1 13 14 22 9",
"M5 54c14-4 20-24 39-18s27-9 40-14 20 11 33 10",
];
---
<svg class="waterbody-mark" width={size} height={Math.round(size * .62)} viewBox="0 0 124 72" aria-hidden="true">
<path class="waterbody-mark__shore" d={shores[variant]} />
<path d="M12 61c18-7 31 6 49-2s31-6 51 0" />
<path d="M24 68c13-4 25 3 39-2s25-3 37 0" />
<circle cx={variant === 1 ? 82 : 51} cy={variant === 2 ? 25 : 30} r="3" />
</svg>
+2 -2
View File
@@ -13,6 +13,6 @@ if (unavailable) {
} }
--- ---
<Layout title="Все виды рыб Russian Fishing 4 — RF4 Spotter" description="Каталог рыб RF4 со свежими точками, уловами, приманками и прозрачными источниками данных."> <Layout title="Все виды рыб Russian Fishing 4 — RF4 Spotter" description="Каталог рыб RF4 со свежими точками, уловами, приманками и прозрачными источниками данных.">
<PageHero eyebrow="Справочник RF4" title="Рыбы" description="Выберите вид, чтобы увидеть свежие подтверждённые точки и полевые сигналы." /> <PageHero eyebrow="Справочник RF4" title="Рыбы" description="Выберите вид, чтобы увидеть свежие подтверждённые точки и полевые сигналы." variant="fish" count={fishes.length} />
{unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенный список. Попробуйте обновить страницу позже." /> : <nav class="catalog-grid content-grid" aria-label="Виды рыб">{fishes.map(fish => <a href={`/fish/${fish.slug}`}><span>Вид рыбы</span><strong>{fish.name_ru}</strong><i></i><FishSilhouette name={fish.name_ru} size={76}/></a>)}</nav>} {unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенный список. Попробуйте обновить страницу позже." /> : <nav class="catalog-grid content-grid" aria-label="Виды рыб">{fishes.map((fish,index) => <a href={`/fish/${fish.slug}`}><span>Вид рыбы · {String(index + 1).padStart(2,"0")}</span><strong>{fish.name_ru}</strong><i>Открыть <b>→</b></i><FishSilhouette name={fish.name_ru} size={92}/></a>)}</nav>}
</Layout> </Layout>
+3 -2
View File
@@ -2,6 +2,7 @@
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import PageHero from "../../components/PageHero.astro"; import PageHero from "../../components/PageHero.astro";
import StatePanel from "../../components/StatePanel.astro"; import StatePanel from "../../components/StatePanel.astro";
import WaterbodyMark from "../../components/WaterbodyMark.astro";
import { api, type DictionaryItem } from "../../lib/api"; import { api, type DictionaryItem } from "../../lib/api";
let waters: DictionaryItem[] = [], unavailable = false; let waters: DictionaryItem[] = [], unavailable = false;
try { waters = await api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"); } catch { unavailable = true; } try { waters = await api<DictionaryItem[]>("/api/v1/waterbodies?limit=500"); } catch { unavailable = true; }
@@ -12,6 +13,6 @@ if (unavailable) {
} }
--- ---
<Layout title="Все водоёмы Russian Fishing 4 — RF4 Spotter" description="Каталог водоёмов RF4 со свежими точками, рыбами, приманками и прозрачными источниками данных."> <Layout title="Все водоёмы Russian Fishing 4 — RF4 Spotter" description="Каталог водоёмов RF4 со свежими точками, рыбами, приманками и прозрачными источниками данных.">
<PageHero eyebrow="Карта водоёмов RF4" title="Водоёмы" description="Откройте водоём, чтобы увидеть активные виды рыб и последние подтверждённые точки." /> <PageHero eyebrow="Карта водоёмов RF4" title="Водоёмы" description="Откройте водоём, чтобы увидеть активные виды рыб и последние подтверждённые точки." variant="water" count={waters.length} />
{unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенный список. Попробуйте обновить страницу позже." /> : <nav class="catalog-grid content-grid" aria-label="Водоёмы">{waters.map(water => <a href={`/waterbodies/${water.slug}`}><span>Водоём</span><strong>{water.name_ru}</strong><i>→</i></a>)}</nav>} {unavailable ? <StatePanel tone="unavailable" title="Каталог временно недоступен" description="Не показываем непроверенный список. Попробуйте обновить страницу позже." /> : <nav class="catalog-grid catalog-grid--waters content-grid" aria-label="Водоёмы">{waters.map((water,index) => <a href={`/waterbodies/${water.slug}`}><span>Водоём · {String(index + 1).padStart(2,"0")}</span><strong>{water.name_ru}</strong><i>Открыть <b>→</b></i><WaterbodyMark name={water.name_ru}/></a>)}</nav>}
</Layout> </Layout>
+4
View File
@@ -1 +1,5 @@
.catalog-hero{padding:68px 0 42px;border-bottom:1px solid #cbd6ce}.catalog-hero h1{margin:12px 0 10px;font:400 clamp(58px,7vw,94px)/.9 Georgia,serif;letter-spacing:-.055em}.catalog-hero p{color:#4d605d;font-size:16px}.catalog-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding:34px 0 100px}.catalog-grid>a{display:grid;grid-template-columns:1fr auto;gap:7px;padding:22px;border:1px solid #d3ddd5;border-radius:14px;background:#fbfcf9;text-decoration:none;transition:.18s ease}.catalog-grid>a:hover{transform:translateY(-2px);border-color:#81958a;box-shadow:0 14px 30px #14333812}.catalog-grid span{grid-column:1/-1;color:#657673;font-size:10px;text-transform:uppercase;letter-spacing:.1em}.catalog-grid strong{font:400 22px Georgia,serif}.catalog-grid i{font-style:normal;color:#6d8d2a}.catalog-results{display:grid;grid-template-columns:230px 1fr;gap:26px;padding:34px 0 100px}.catalog-results aside{padding:20px;background:#fff;border:1px solid #d6dfd7;border-radius:14px;align-self:start}.catalog-results aside nav{display:flex;flex-direction:column;margin-top:12px}.catalog-results aside a{padding:10px 0;border-bottom:1px solid #e1e8e2;color:#405552;font-size:13px}.catalog-results>div{display:grid;gap:12px}.catalog-results--single{display:block;max-width:960px}.catalog-results .spot-card{grid-template-columns:45px 1fr 190px}@media(max-width:900px){.catalog-grid{grid-template-columns:repeat(2,1fr)}.catalog-results{grid-template-columns:1fr}.catalog-results aside nav{display:grid;grid-template-columns:repeat(2,1fr);gap:0 16px}}@media(max-width:720px){.catalog-grid{grid-template-columns:1fr;padding-bottom:70px}.catalog-results{padding-bottom:70px}.catalog-results .spot-card{grid-template-columns:34px 1fr}.topbar nav{gap:8px}.topbar nav a{font-size:12px}} .catalog-hero{padding:68px 0 42px;border-bottom:1px solid #cbd6ce}.catalog-hero h1{margin:12px 0 10px;font:400 clamp(58px,7vw,94px)/.9 Georgia,serif;letter-spacing:-.055em}.catalog-hero p{color:#4d605d;font-size:16px}.catalog-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding:34px 0 100px}.catalog-grid>a{display:grid;grid-template-columns:1fr auto;gap:7px;padding:22px;border:1px solid #d3ddd5;border-radius:14px;background:#fbfcf9;text-decoration:none;transition:.18s ease}.catalog-grid>a:hover{transform:translateY(-2px);border-color:#81958a;box-shadow:0 14px 30px #14333812}.catalog-grid span{grid-column:1/-1;color:#657673;font-size:10px;text-transform:uppercase;letter-spacing:.1em}.catalog-grid strong{font:400 22px Georgia,serif}.catalog-grid i{font-style:normal;color:#6d8d2a}.catalog-results{display:grid;grid-template-columns:230px 1fr;gap:26px;padding:34px 0 100px}.catalog-results aside{padding:20px;background:#fff;border:1px solid #d6dfd7;border-radius:14px;align-self:start}.catalog-results aside nav{display:flex;flex-direction:column;margin-top:12px}.catalog-results aside a{padding:10px 0;border-bottom:1px solid #e1e8e2;color:#405552;font-size:13px}.catalog-results>div{display:grid;gap:12px}.catalog-results--single{display:block;max-width:960px}.catalog-results .spot-card{grid-template-columns:45px 1fr 190px}@media(max-width:900px){.catalog-grid{grid-template-columns:repeat(2,1fr)}.catalog-results{grid-template-columns:1fr}.catalog-results aside nav{display:grid;grid-template-columns:repeat(2,1fr);gap:0 16px}}@media(max-width:720px){.catalog-grid{grid-template-columns:1fr;padding-bottom:70px}.catalog-results{padding-bottom:70px}.catalog-results .spot-card{grid-template-columns:34px 1fr}.topbar nav{gap:8px}.topbar nav a{font-size:12px}}
/* Editorial atlas: catalog pages read as a field guide, not a utility list. */
.catalog-hero--illustrated{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 210px;align-items:center;gap:48px;min-height:310px;overflow:hidden}.catalog-hero--illustrated:before{content:"";position:absolute;right:102px;width:330px;height:190px;border:1px solid color-mix(in srgb,var(--teal) 11%,transparent);border-radius:50%;box-shadow:0 0 0 32px color-mix(in srgb,var(--teal) 5%,transparent),0 0 0 64px color-mix(in srgb,var(--teal) 3%,transparent);transform:rotate(-9deg);pointer-events:none}.catalog-hero__copy{position:relative;z-index:1}.catalog-hero__copy p{max-width:660px;line-height:1.6}.catalog-hero__seal{position:relative;z-index:1;width:176px;height:176px;justify-self:end;display:grid;place-items:center;align-content:center;border:1px solid #345155;border-radius:50%;background:var(--deep);color:var(--lime);box-shadow:0 18px 48px #16383c1b}.catalog-hero__seal>span{height:60px;display:grid;place-items:center}.catalog-hero__seal .fish-silhouette{color:var(--lime)}.catalog-hero__seal strong{margin-top:5px;font:400 32px/1 Georgia,serif;color:var(--white)}.catalog-hero__seal small{margin-top:4px;color:#a9b8b5;font-size:9px;text-transform:uppercase;letter-spacing:.13em}.catalog-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,330px),1fr));gap:14px}.catalog-grid>a{min-height:146px;align-content:start;padding:24px 25px;border-color:var(--border-soft);background:linear-gradient(145deg,var(--surface),var(--surface-soft))}.catalog-grid>a:before{content:"";position:absolute;inset:0;border-top:3px solid transparent;transition:border-color var(--motion-base) var(--ease-out)}.catalog-grid>a:hover:before{border-color:var(--lime)}.catalog-grid>a>span{color:var(--text-muted)}.catalog-grid>a>strong{position:relative;z-index:1;margin-top:10px;font-size:28px}.catalog-grid>a>i{position:relative;z-index:1;align-self:end;display:flex;align-items:center;gap:8px;margin-top:20px;color:var(--text-muted);font:700 10px Inter,sans-serif;text-transform:uppercase;letter-spacing:.09em}.catalog-grid>a>i b{color:#6d8d2a;font-size:16px}.catalog-grid>a>.fish-silhouette{right:20px;bottom:12px;opacity:.095}.waterbody-mark{position:absolute;right:14px;bottom:8px;fill:none;stroke:#315f63;stroke-width:1.2;opacity:.12;pointer-events:none}.waterbody-mark__shore{stroke-width:2}.waterbody-mark circle{fill:var(--lime);stroke:none}
@media(max-width:720px){.catalog-hero--illustrated{grid-template-columns:1fr 106px;gap:18px;min-height:230px}.catalog-hero--illustrated:before{right:-35px;width:210px;height:120px}.catalog-hero__seal{width:96px;height:96px}.catalog-hero__seal>span{height:38px}.catalog-hero__seal .fish-icon{width:36px;height:36px}.catalog-hero__seal .fish-silhouette{width:62px}.catalog-hero__seal strong{font-size:20px}.catalog-hero__seal small{display:none}.catalog-grid>a{min-height:132px}.catalog-grid>a>strong{font-size:25px}}
+1 -1
View File
@@ -12,7 +12,7 @@
@immutable path /_astro/* @immutable path /_astro/*
header @immutable Cache-Control "public, max-age=31536000, immutable" header @immutable Cache-Control "public, max-age=31536000, immutable"
@brandAssets path /favicon.svg /favicon-32.png /apple-touch-icon.png /icon-192.png /icon-512.png /og-rf4spotter.png @brandAssets path /favicon.svg /favicon-32.png /apple-touch-icon.png /icon-192.png /icon-512.png /icon-maskable-192.png /icon-maskable-512.png /og-rf4spotter.png
header @brandAssets Cache-Control "public, max-age=86400, stale-while-revalidate=604800" header @brandAssets Cache-Control "public, max-age=86400, stale-while-revalidate=604800"
@manifest path /site.webmanifest @manifest path /site.webmanifest
+20 -1
View File
@@ -25,7 +25,12 @@
- [x] **B03 · Графическая грамматика.** Мотивы закреплены за функциями: крючок — бренд, поплавок — активность/ожидание, леска — время, радар — координаты, силуэт — сущность рыбы. Убраны ложная рябь неполных сигналов и дублирующие радар круги карточки лидера; неполнота теперь подчёркнута спокойной полевой меткой. - [x] **B03 · Графическая грамматика.** Мотивы закреплены за функциями: крючок — бренд, поплавок — активность/ожидание, леска — время, радар — координаты, силуэт — сущность рыбы. Убраны ложная рябь неполных сигналов и дублирующие радар круги карточки лидера; неполнота теперь подчёркнута спокойной полевой меткой.
- [x] **B04 · Компонентная подпись.** `SectionHeading`, `PageHero` и `StatePanel` унифицируют заголовки, hero и состояния; семантическая иерархия `data-action` согласует CTA публичных и admin-страниц. - [x] **B04 · Компонентная подпись.** `SectionHeading`, `PageHero` и `StatePanel` унифицируют заголовки, hero и состояния; семантическая иерархия `data-action` согласует CTA публичных и admin-страниц.
- [x] **B05 · Motion-система.** Смысловые микроанимации пульса, поплавка, загрузки и интерактивного отклика используют общие duration/easing-токены; декоративное движение empty-state удалено, reduced motion покрывает элементы и псевдоэлементы. - [x] **B05 · Motion-система.** Смысловые микроанимации пульса, поплавка, загрузки и интерактивного отклика используют общие duration/easing-токены; декоративное движение empty-state удалено, reduced motion покрывает элементы и псевдоэлементы.
- [ ] **B06 · Brand QA.** Проверить favicon, maskable icons, manifest, OG 1200×630, светлый/тёмный фон, 16/32 px и mobile/desktop screenshots; записать правила в [brand-system.md](brand-system.md). - [x] **B06 · Brand QA.** Desktop/mobile-проверка усилила каталоги atlas-hero, счётчиками, береговыми контурами и выразительными карточками. Favicon/PWA PNG перегенерированы из SVG-мастера без размытия; `any` и полнофоновые `maskable`-иконки разделены, manifest и Caddy cache обновлены, OG подтверждён как 1200×630.
- [x] **B07 · Аудит официальных изображений.** Текущий records-parser получает названия рыб/водоёмов и `title` приманки, но не image URL; официальные gallery/userguide содержат тематические медиа без устойчивого полного соответствия каноническим сущностям. Изображения не хотлинкать и не считать разрешение на данные автоматическим разрешением на медиапубликацию.
- [ ] **B08 · Семейства силуэтов рыб.** Заменить три случайных варианта по hash имени на осмысленные `visual_family`: щуковые, карповые, лососёвые, окуневые, сомовые, угреобразные, плоские и морские. Название остаётся главным идентификатором; семейство хранится отдельно и допускает ручную коррекцию.
- [ ] **B09 · Глифы снастей и приманок.** Ввести компактные SVG-глифы по типу (`spinner`, `wobbler`, `soft`, `boilie`, `worm`, `hook`, `rig`, `unknown`) и детерминированный цвет по normalized name. Не изображать конкретную модель, если известна только строка из рекорда.
- [ ] **B10 · Визуальные отпечатки водоёмов.** Расширить текущие три береговых контура до детерминированного набора по slug: форма берега, число волн, положение точки и индексный код. Явно обозначать их как абстрактный знак, не карту и не игровую географию.
- [ ] **B11 · Опциональный media pipeline.** После отдельного письменного разрешения на изображения добавить allowlist официальных asset URL, локальное сохранение без hotlink, content hash, MIME/dimensions, attribution/source URL, дату получения и fallback на SVG. Изменение/удаление оригинала должно отправлять media asset на повторную проверку, а не молча менять публичную карточку.
- [ ] **Q01 · Документы источников.** Приложить или дать устойчивые ссылки на первичные разрешения RF4DB, RF4-STAT, RF4MAP и RF4 Posts; для каждого зафиксировать атрибуцию, точный production-лимит, срок хранения и процедуру удаления. - [ ] **Q01 · Документы источников.** Приложить или дать устойчивые ссылки на первичные разрешения RF4DB, RF4-STAT, RF4MAP и RF4 Posts; для каждого зафиксировать атрибуцию, точный production-лимит, срок хранения и процедуру удаления.
- [ ] **Q02 · Управляемое удаление источника.** Добавить обнаружение изменённых/удалённых опубликованных записей без отдельного частого обхода: статус, журнал решения и безопасное исключение из активности после проверки. - [ ] **Q02 · Управляемое удаление источника.** Добавить обнаружение изменённых/удалённых опубликованных записей без отдельного частого обхода: статус, журнал решения и безопасное исключение из активности после проверки.
@@ -38,6 +43,19 @@
- [ ] **Q09 · Release-процедура.** Разделить миграционный/release-шаг и запуск приложения либо документировать выбранную стратегию отката; проверить upgrade с предыдущей ревизии на копии данных. - [ ] **Q09 · Release-процедура.** Разделить миграционный/release-шаг и запуск приложения либо документировать выбранную стратегию отката; проверить upgrade с предыдущей ревизии на копии данных.
- [ ] **Q10 · Документальная ревизия.** После каждого пакета обновлять этот файл и README, не возвращая закрытые R/A/T-задачи в активный backlog. - [ ] **Q10 · Документальная ревизия.** После каждого пакета обновлять этот файл и README, не возвращая закрытые R/A/T-задачи в активный backlog.
### Дополнения после ревизии PROJECT_AUDIT_2026-09-10
Аудит выполнен на старой базе `13e04e6`; рекомендации ниже повторно проверены по текущей ветке. Уже реализованные или неприменимые предложения не возвращаются в backlog.
- [ ] **Q11 · Декомпозиция API.** Инкрементально разделить `apps/api/app/main.py` (638 строк, 26 маршрутов) на `APIRouter` по публичному каталогу/activity, submissions и admin/community. Сначала зафиксировать OpenAPI snapshot и сохранить URL, response models, middleware и dependency-поведение без функциональных изменений.
- [ ] **Q12 · Query-plan gate.** В рамках Q07 снять `EXPLAIN (ANALYZE, BUFFERS)` для activity, records, spot detail и public spot pages на реалистичном наборе данных. Существующие индексы миграции `0011_query_indexes` не дублировать; индекс с `fish_id`, SQL-агрегацию или materialized view добавлять только по измеренному плану и p95.
- [ ] **Q13 · Production bootstrap в CI.** Добавить отдельный ручной/плановый job для `deploy/test-production-bootstrap.sh`, не удваивая быстрый Compose E2E на каждом коммите; сохранять diagnostics при падении.
- [ ] **Q14 · Полная CSP.** Расширить текущую CSP (`frame-ancestors`, `base-uri`, `object-src`) до `default-src`, `script-src`, `style-src`, `img-src`, `connect-src` и `form-action`. Сначала инвентаризировать inline scripts/styles Astro, затем внедрить nonce/hash или безопасное вынесение; проверить report/admin/OG без ослабления до произвольных внешних origin.
- [ ] **Q15 · Частичная деградация главной.** Разделить получение activity, community signals и справочников так, чтобы отказ одного источника не превращал всю главную в общий 503. Для SSR не добавлять искусственный client-side loading/optimistic UI; показывать независимые `StatePanel` и корректный HTTP/cache статус.
- [ ] **Q16 · Контракт OpenAPI.** Генерировать OpenAPI artifact из приложения в CI и проверять осознанные изменения контракта при декомпозиции API; не поддерживать вручную редактируемую копию.
- [ ] **Q17 · Эксплуатационные документы.** Добавить короткие ADR по Astro/FastAPI/PostgreSQL, локальному cache и стратегии scheduler, а также incident runbook для заполнения диска/PostgreSQL, отказа MinIO, зависших импортов, ошибок миграции и компрометации секретов.
- [ ] **Q18 · Минимальная observability.** До открытой альфы определить дешёвые метрики request count/latency/error rate, глубины moderation/staging и возраста последнего успешного импорта. Формат и exporter выбрать после выбора мониторинга сервера; полноценный tracing не внедрять без подтверждённой потребности.
## Готовность открытой альфы — требуется сервер или внешний сервис ## Готовность открытой альфы — требуется сервер или внешний сервис
- [ ] Купить/подготовить Linux-сервер и подтвердить его публичный IPv4/IPv6. - [ ] Купить/подготовить Linux-сервер и подтвердить его публичный IPv4/IPv6.
@@ -55,6 +73,7 @@
- [ ] Решить по обратной связи, нужны ли OCR, Telegram, профили и уведомления о клёве. - [ ] Решить по обратной связи, нужны ли OCR, Telegram, профили и уведомления о клёве.
- [ ] Рассмотреть cursor pagination при росте объёмов; текущая offset pagination достаточна для альфы. - [ ] Рассмотреть cursor pagination при росте объёмов; текущая offset pagination достаточна для альфы.
- [ ] Рассмотреть общую инвалидацию кэша при нескольких API-процессах; сейчас действует TTL и локальная инвалидация. - [ ] Рассмотреть общую инвалидацию кэша при нескольких API-процессах; сейчас действует TTL и локальная инвалидация.
- [ ] Рассмотреть Redis только при переходе к нескольким API-процессам или после измеренного дефицита локального cache; не добавлять отдельный stateful-сервис заранее.
- [ ] Подготовить тематические обложки и индивидуальные OG для рыб/водоёмов, если страницы подтверждают поисковую ценность. - [ ] Подготовить тематические обложки и индивидуальные OG для рыб/водоёмов, если страницы подтверждают поисковую ценность.
## Правила выполнения ## Правила выполнения
+9 -1
View File
@@ -50,10 +50,18 @@ CTA задаются смыслом через `data-action`: `primary` — ос
Движение объясняет состояние: пульс означает живой сигнал, поплавок — активность точки, sweep — загрузку, короткий сдвиг — отклик интерактивного элемента. Декоративные бесконечные анимации не используются. Длительности берутся из `--motion-fast`, `--motion-base`, `--motion-signal`, easing — из `--ease-out`; `prefers-reduced-motion` сводит все анимации и переходы к одному кадру. Движение объясняет состояние: пульс означает живой сигнал, поплавок — активность точки, sweep — загрузку, короткий сдвиг — отклик интерактивного элемента. Декоративные бесконечные анимации не используются. Длительности берутся из `--motion-fast`, `--motion-base`, `--motion-signal`, easing — из `--ease-out`; `prefers-reduced-motion` сводит все анимации и переходы к одному кадру.
## Каталог как полевой атлас
Каталоги используют собственную редакционную композицию: круглый тёмный seal показывает тип и объём справочника, порядковые номера создают ощущение системного атласа. Водоём обозначается абстрактным береговым контуром и точкой наблюдения, рыба — силуэтом. Эти фоновые знаки всегда декоративны, имеют низкий контраст и не заменяют текстовое название.
## Brand assets и QA
`favicon.svg` — мастер обычных favicon, Apple Touch и PWA `purpose: any`; PNG генерируются из него напрямую в целевом размере, а не масштабируются из малого растра. `icon-maskable.svg` — полнофоновый мастер для отдельных `purpose: maskable` 192/512 px, чтобы системная маска не открывала прозрачные углы. OG имеет ровно 1200×630 px и использует тот же тёмный водный фон, лаймовый поплавок и координатную сетку. Проверены desktop и 390 px каталоги, 32/180/192/512 px assets, светлая поверхность сайта и тёмные бренд-носители.
Секции данных используют `SectionHeading`: короткий uppercase eyebrow задаёт контекст, антиквенный заголовок называет сущность, необязательное описание объясняет правила, а счётчик всегда расположен справа или под текстом на мобильном. Так число результатов не конкурирует с названием раздела и одинаково читается в рекордах и полевых сигналах. Секции данных используют `SectionHeading`: короткий uppercase eyebrow задаёт контекст, антиквенный заголовок называет сущность, необязательное описание объясняет правила, а счётчик всегда расположен справа или под текстом на мобильном. Так число результатов не конкурирует с названием раздела и одинаково читается в рекордах и полевых сигналах.
Каталоги, detail-выборки и страницы прозрачности используют `PageHero`: один контекст, один `h1`, одно короткое пояснение. Это сохраняет одинаковый вертикальный ритм и SEO-иерархию независимо от наличия данных. Каталоги, detail-выборки и страницы прозрачности используют `PageHero`: один контекст, один `h1`, одно короткое пояснение. Это сохраняет одинаковый вертикальный ритм и SEO-иерархию независимо от наличия данных.
## План внедрения ## План внедрения
Рабочие статусы B01–B06 ведутся в [ROADMAP.md](ROADMAP.md). После каждого пакета выполняются Astro check/build и адресная визуальная проверка 320/390/768/1280 px. Новые bitmap-иллюстрации не добавляются, пока существующие SVG/CSS-мотивы решают задачу без лишнего веса. Рабочие статусы B01–B06 ведутся в [ROADMAP.md](ROADMAP.md). После каждого пакета выполняются Astro check/build и адресная визуальная проверка ключевых desktop/mobile-композиций. Новые bitmap-иллюстрации не добавляются, пока существующие SVG/CSS-мотивы решают задачу без лишнего веса.
+10
View File
@@ -46,6 +46,16 @@
| `record_date` | `.data`, формат `D.MM.YY`/`DD.MM.YY` | | `record_date` | `.data`, формат `D.MM.YY`/`DD.MM.YY` |
| `source_url` | URL полученной страницы | | `source_url` | URL полученной страницы |
## Изображения официального сайта
Проверка 12 сентября 2026 года не обнаружила стабильного публичного каталога вида `канонический slug → изображение` для рыб, водоёмов или снастей. Официальная [галерея водоёмов](https://rf4game.de/media/levels/) и категория [рыб](https://rf4game.de/media/fische/) содержат тематические публикации, а [руководство](https://rf4game.de/userguide/) — иллюстрации интерфейса, оснасток и карт. Это медиа-галереи, а не полный справочник с устойчивыми entity ID.
Страница рекордов визуально использует `.bait_icon`, но подтверждённый parser-контракт извлекает только `.bait_icon[title]`. Ни parser, ни `Fish`/`Waterbody`/`Bait` модели и API-схемы сейчас не сохраняют image URL. Даже если URL удастся извлечь из CSS или обновлённого DOM, он может быть presentation asset, меняться независимо от названия и покрывать только приманки, попавшие в рекорды.
До отдельного разрешения на медиапубликацию изображения официального сайта не скачиваются, не хотлинкаются и не отображаются как собственные. Разрешение использовать фактические данные не трактуется как разрешение републиковать графические произведения.
Fallback строится на собственных лёгких SVG: осмысленные семейства силуэтов рыб, типовые глифы снастей и детерминированные абстрактные отпечатки водоёмов. Текстовое название всегда остаётся основным идентификатором; абстрактный контур водоёма не называется картой, а глиф приманки — изображением конкретной модели.
Координат, проводки, времени поимки и устойчивого внешнего идентификатора записи в этой таблице нет. Локализация зависит от домена/языка страницы: проверенная `.de`-страница возвращает немецкие названия рыб, водоёмов и приманок даже для региона RU. Координат, проводки, времени поимки и устойчивого внешнего идентификатора записи в этой таблице нет. Локализация зависит от домена/языка страницы: проверенная `.de`-страница возвращает немецкие названия рыб, водоёмов и приманок даже для региона RU.
## Рекомендуемый режим получения ## Рекомендуемый режим получения