feat: publish open alpha policies
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-07 09:29:26 +07:00
parent 16e4c071d1
commit 9e71215bd8
9 changed files with 32 additions and 5 deletions
+1 -1
View File
@@ -15,6 +15,6 @@ const path = Astro.url.pathname;
<p class="live-badge"><span></span> Свежие данные и честная оценка</p> <p class="live-badge"><span></span> Свежие данные и честная оценка</p>
</header> </header>
<main id="main-content" tabindex="-1"><slot /></main> <main id="main-content" tabindex="-1"><slot /></main>
<footer><a href="/" class="brand"><span class="brand-mark"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a><p>Неофициальный проект для игроков Russian Fishing 4. Данные seed демонстрационные.</p><span>НХНЧ · 2026</span></footer> <footer><a href="/" class="brand"><span class="brand-mark"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><span>Ни хвоста,</span><strong>ни чешуи</strong></span></a><p>Неофициальный проект для игроков Russian Fishing 4. <a href="/rules">Правила</a> · <a href="/privacy">Конфиденциальность</a></p><span>НХНЧ · 2026</span></footer>
</body> </body>
</html> </html>
+12
View File
@@ -0,0 +1,12 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="Конфиденциальность — RF4 Spotter">
<article class="legal-page"><span class="eyebrow">Открытая альфа · 7 сентября 2026</span><h1>Конфиденциальность</h1>
<p>RF4 Spotter собирает только сведения, необходимые для публикации и проверки рыболовных наблюдений, защиты от злоупотреблений и работы сервиса.</p>
<h2>Что обрабатывается</h2><ul><li>рыба, водоём, координаты, вес и необязательные сведения об улове;</li><li>необязательные ник и скриншот;</li><li>HMAC-отпечаток адреса для ограничения частоты — исходный IP не хранится;</li><li>источник, внешний идентификатор и URL импортированных наблюдений.</li></ul>
<h2>Защита и сроки</h2><p>Все заявки проходят модерацию, из изображений удаляются метаданные, база и объектное хранилище не публикуются напрямую. Непроверенные заявки хранятся до 30 дней, персональные поля одобренных — до 180 дней, непубликуемый staging — до 90 дней.</p>
<h2>Удаление и жалобы</h2><p>Для удаления своей заявки напишите на <a href="mailto:privacy@rf4spotter.ru">privacy@rf4spotter.ru</a>, приложив её идентификатор и сведения для поиска. О злоупотреблении сообщайте на <a href="mailto:abuse@rf4spotter.ru">abuse@rf4spotter.ru</a>.</p>
<h2>Сторонние источники</h2><p>Лицензия проекта не передаёт права на игровые материалы, сторонние данные или пользовательские изображения. Community-данные сначала попадают в staging и проверяются вручную.</p>
</article>
</Layout>
+1 -1
View File
@@ -15,7 +15,7 @@ const reportId = Astro.url.searchParams.get("report_id");
{state === "screenshot_error" && reportId && <form class="screenshot-retry" method="post" action="/api/report-screenshot" enctype="multipart/form-data"><input type="hidden" name="report_id" value={reportId} /><label>Повторная загрузка скриншота<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" required /></label><button type="submit">Загрузить скриншот</button></form>} {state === "screenshot_error" && reportId && <form class="screenshot-retry" method="post" action="/api/report-screenshot" enctype="multipart/form-data"><input type="hidden" name="report_id" value={reportId} /><label>Повторная загрузка скриншота<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" required /></label><button type="submit">Загрузить скриншот</button></form>}
{unavailable ? <div class="state"><h2>Форма временно недоступна</h2></div> : <form class="report-form" method="post" action="/api/report" enctype="multipart/form-data"> {unavailable ? <div class="state"><h2>Форма временно недоступна</h2></div> : <form class="report-form" method="post" action="/api/report" enctype="multipart/form-data">
<fieldset><legend>Главное <span>обязательно</span></legend><div class="form-grid"><label>Рыба *<select name="fish_slug" required>{fishes.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Водоём *<select name="waterbody_slug" required>{waterbodies.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Координата X *<input name="x" type="number" min="-10000" max="10000" placeholder="Например, 72" required /></label><label>Координата Y *<input name="y" type="number" min="-10000" max="10000" placeholder="Например, 84" required /></label><label>Вес, граммы *<input name="weight_g" type="number" min="1" max="3000000" placeholder="1250" required /></label><label>Приманка<input name="bait_name" maxlength="200" placeholder="Название в игре" /></label></div><p class="field-help">Координаты — два целых числа с карты. Вес: 1,25 кг = 1250 г.</p></fieldset> <fieldset><legend>Главное <span>обязательно</span></legend><div class="form-grid"><label>Рыба *<select name="fish_slug" required>{fishes.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Водоём *<select name="waterbody_slug" required>{waterbodies.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Координата X *<input name="x" type="number" min="-10000" max="10000" placeholder="Например, 72" required /></label><label>Координата Y *<input name="y" type="number" min="-10000" max="10000" placeholder="Например, 84" required /></label><label>Вес, граммы *<input name="weight_g" type="number" min="1" max="3000000" placeholder="1250" required /></label><label>Приманка<input name="bait_name" maxlength="200" placeholder="Название в игре" /></label></div><p class="field-help">Координаты — два целых числа с карты. Вес: 1,25 кг = 1250 г.</p></fieldset>
<details class="optional-fields"><summary>Дополнительные сведения <span>необязательно</span></summary><div class="form-grid"><label>Способ ловли<select name="fishing_method"><option value="">Не указан</option><option value="spinning">Спиннинг</option><option value="bottom">Донная</option><option value="float">Поплавочная</option></select></label><label>Проводка<input name="retrieve_method" maxlength="100" /></label><label>Скорость проводки<input name="retrieve_speed" type="number" min="0" max="100" /></label><label>Ник игрока<input name="player_name" maxlength="100" /></label></div><label class="wide">Комментарий<textarea name="comment" maxlength="1000" rows="4"></textarea></label><label class="wide">Скриншот, JPEG/PNG/WebP до 8 МБ<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" /></label></details><label class="honeypot" aria-hidden="true">Сайт<input name="website" tabindex="-1" autocomplete="off" /></label><p class="privacy">Ник и скриншот необязательны. Из изображения удаляются EXIF и прочие метаданные.</p><button type="submit">Отправить на проверку</button> <details class="optional-fields"><summary>Дополнительные сведения <span>необязательно</span></summary><div class="form-grid"><label>Способ ловли<select name="fishing_method"><option value="">Не указан</option><option value="spinning">Спиннинг</option><option value="bottom">Донная</option><option value="float">Поплавочная</option></select></label><label>Проводка<input name="retrieve_method" maxlength="100" /></label><label>Скорость проводки<input name="retrieve_speed" type="number" min="0" max="100" /></label><label>Ник игрока<input name="player_name" maxlength="100" /></label></div><label class="wide">Комментарий<textarea name="comment" maxlength="1000" rows="4"></textarea></label><label class="wide">Скриншот, JPEG/PNG/WebP до 8 МБ<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" /></label></details><label class="honeypot" aria-hidden="true">Сайт<input name="website" tabindex="-1" autocomplete="off" /></label><p class="privacy">Ник и скриншот необязательны. Из изображения удаляются EXIF и прочие метаданные.</p><label class="consent"><input name="consent" type="checkbox" required /> <span>Я отправляю собственное наблюдение и принимаю <a href="/rules" target="_blank">правила</a> и <a href="/privacy" target="_blank">политику конфиденциальности</a>.</span></label><button type="submit">Отправить на проверку</button>
</form>} </form>}
<script is:inline define:vars={{ state }}> <script is:inline define:vars={{ state }}>
const form = document.querySelector(".report-form"); const key = "rf4-report-draft"; const form = document.querySelector(".report-form"); const key = "rf4-report-draft";
+12
View File
@@ -0,0 +1,12 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="Правила открытой альфы — RF4 Spotter">
<article class="legal-page"><span class="eyebrow">Открытая альфа · 7 сентября 2026</span><h1>Правила участия</h1>
<p>RF4 Spotter — неофициальный экспериментальный сервис сообщества. Данные могут быть неполными или устаревшими и не гарантируют результат в игре.</p>
<h2>Можно</h2><ul><li>отправлять собственные достоверные наблюдения из Russian Fishing 4;</li><li>указывать необязательный ник и прикладывать свой скриншот;</li><li>сообщать об ошибках, устаревших точках и злоупотреблениях.</li></ul>
<h2>Нельзя</h2><ul><li>выдавать чужие материалы за свои или публиковать персональные данные;</li><li>размещать рекламу, оскорбления или вредоносный контент;</li><li>автоматически отправлять заявки и обходить ограничения частоты;</li><li>использовать сервис для нарушения правил RF4.</li></ul>
<h2>Модерация</h2><p>Любая заявка может быть отклонена или удалена. До одобрения она не влияет на статистику. При злоупотреблении доступ может быть ограничен.</p>
<h2>Контакты</h2><p>Удаление и данные: <a href="mailto:privacy@rf4spotter.ru">privacy@rf4spotter.ru</a>. Жалобы: <a href="mailto:abuse@rf4spotter.ru">abuse@rf4spotter.ru</a>.</p>
</article>
</Layout>
+1
View File
@@ -27,3 +27,4 @@ footer{min-height:118px;background:var(--deep);color:#dbe4df;padding:28px max(32
@media(max-width:720px){.record-row:not(.record-head)>*{display:grid;grid-template-columns:88px 1fr;text-align:left!important;gap:8px}.record-row:not(.record-head)>*:before{content:attr(data-label);font:700 9px Inter,sans-serif;text-transform:uppercase;letter-spacing:.08em;color:#82908e}.record-row{grid-template-columns:1fr}.record-filters>a{height:auto;padding:8px}} @media(max-width:720px){.record-row:not(.record-head)>*{display:grid;grid-template-columns:88px 1fr;text-align:left!important;gap:8px}.record-row:not(.record-head)>*:before{content:attr(data-label);font:700 9px Inter,sans-serif;text-transform:uppercase;letter-spacing:.08em;color:#82908e}.record-row{grid-template-columns:1fr}.record-filters>a{height:auto;padding:8px}}
/* WCAG AA secondary text on light surfaces */ /* WCAG AA secondary text on light surfaces */
.eyebrow,.overline,.result-count,.spot-rank,.spot-topline,.spot-meta,.bait-line div span,.spot-stats span,.data-note,.state,.principles p,.principles article>span,.official-note,.privacy,.field-help,.report-form legend span,.optional-fields summary span,.record-row>span,.record-row>time{color:#4d605d} .eyebrow,.overline,.result-count,.spot-rank,.spot-topline,.spot-meta,.bait-line div span,.spot-stats span,.data-note,.state,.principles p,.principles article>span,.official-note,.privacy,.field-help,.report-form legend span,.optional-fields summary span,.record-row>span,.record-row>time{color:#4d605d}
.consent{display:flex!important;align-items:flex-start;gap:10px;margin:20px 0;text-transform:none!important;letter-spacing:0!important;font-size:14px!important;line-height:1.45}.consent input{width:20px;height:20px;margin:0;flex:none}.legal-page{width:min(760px,calc(100% - 32px));margin:60px auto 100px}.legal-page h1{font:400 clamp(48px,7vw,82px)/.95 Georgia,serif;letter-spacing:-.05em}.legal-page h2{margin-top:36px;font:400 28px Georgia,serif}.legal-page p,.legal-page li{color:#405552;line-height:1.7}.legal-page a{text-underline-offset:3px}
+1 -1
View File
@@ -1,7 +1,7 @@
import AxeBuilder from "@axe-core/playwright"; import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
for (const path of ["/", "/records", "/report"]) { for (const path of ["/", "/records", "/report", "/rules", "/privacy"]) {
test(`${path} has no serious accessibility violations`, async ({ page }) => { test(`${path} has no serious accessibility violations`, async ({ page }) => {
await page.goto(path); await page.goto(path);
const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze(); const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze();
@@ -11,6 +11,7 @@ test("production bootstrap supports submission and moderation", async ({ page, r
await page.getByLabel("Координата X *").fill(String(x)); await page.getByLabel("Координата X *").fill(String(x));
await page.getByLabel("Координата Y *").fill(String(y)); await page.getByLabel("Координата Y *").fill(String(y));
await page.getByLabel("Вес, граммы *").fill("4321"); await page.getByLabel("Вес, граммы *").fill("4321");
await page.getByLabel(/Я отправляю собственное наблюдение/).check();
await page.getByLabel("Ник игрока").fill(player); await page.getByLabel("Ник игрока").fill(player);
await page.getByRole("button", { name: "Отправить на проверку" }).click(); await page.getByRole("button", { name: "Отправить на проверку" }).click();
await expect(page.getByText("Улов отправлен на модерацию. Спасибо!")).toBeVisible(); await expect(page.getByText("Улов отправлен на модерацию. Спасибо!")).toBeVisible();
+1
View File
@@ -25,6 +25,7 @@ test("submitted catch appears publicly only after moderation", async ({ page })
await page.getByLabel("Координата Y *").fill(String(y)); await page.getByLabel("Координата Y *").fill(String(y));
await page.getByLabel("Вес, граммы *").fill("6789"); await page.getByLabel("Вес, граммы *").fill("6789");
await page.getByLabel("Ник игрока").fill(player); await page.getByLabel("Ник игрока").fill(player);
await page.getByLabel(/Я отправляю собственное наблюдение/).check();
await page.getByRole("button", { name: "Отправить на проверку" }).click(); await page.getByRole("button", { name: "Отправить на проверку" }).click();
await expect(page.getByText("Улов отправлен на модерацию. Спасибо!")).toBeVisible(); await expect(page.getByText("Улов отправлен на модерацию. Спасибо!")).toBeVisible();
+2 -2
View File
@@ -107,7 +107,7 @@
## Этап 5 — пилот ## Этап 5 — пилот
- [ ] Опубликовать правила открытой альфы и privacy notice, добавить явное согласие перед отправкой улова. - [x] Опубликовать страницы `/rules` и `/privacy`, добавить ссылки в footer и обязательное несохраняемое согласие перед отправкой улова.
- [ ] Создать и проверить ящики `privacy@rf4spotter.ru` и `abuse@rf4spotter.ru`, определить срок реакции на обращения. - [ ] Создать и проверить ящики `privacy@rf4spotter.ru` и `abuse@rf4spotter.ru`, определить срок реакции на обращения.
- [ ] Проверить публичные abuse-сценарии: burst заявок, крупные файлы, повторные отправки и рост очереди модерации. - [ ] Проверить публичные abuse-сценарии: burst заявок, крупные файлы, повторные отправки и рост очереди модерации.
- [ ] Зафиксировать короткий сценарий приёмки пилота и измеримые критерии: успешная отправка/модерация, понятность оценки, свежесть данных и допустимое время ответа. - [ ] Зафиксировать короткий сценарий приёмки пилота и измеримые критерии: успешная отправка/модерация, понятность оценки, свежесть данных и допустимое время ответа.
@@ -122,7 +122,7 @@
Технический production-контур, health/readiness, backup/restore и безопасные логи готовы. Следующие пункты выполняются строго по одному: Технический production-контур, health/readiness, backup/restore и безопасные логи готовы. Следующие пункты выполняются строго по одному:
1. добавить публичные страницы правил и privacy notice, согласие формы и контакты `privacy`/`abuse`; 1. создать и проверить контакты `privacy`/`abuse`;
2. подготовить сценарий и измеримые критерии открытой альфы, включая публичную нагрузку и злоупотребления; 2. подготовить сценарий и измеримые критерии открытой альфы, включая публичную нагрузку и злоупотребления;
3. мониторинг, DNS/TLS и проверка production-профиля на целевом сервере. 3. мониторинг, DNS/TLS и проверка production-профиля на целевом сервере.