docs(generator): issue 09 и 10 дооформлены до ready-for-agent
- Зачем:
- закрыть последний шаг триажа: без Acceptance criteria и способа приёмки
задачи нельзя передавать Codex на исполнение.
- Что:
- issue 09: причина уточнена по коду (расходится источник фактуры — донор
против seed_click_id, а не индекс), зафиксировано решение хранить донора
в state, критерии расширены полями location (referer, utm) по итогам ревью.
- issue 10: приёмка по скриншотам playwright-cli с определением «читаемо»,
поправлена длительность пересборки (6 часов по умолчанию, не 2 суток),
отмечено, что legacy world_map, похоже, не умеет легенду и tooltip.
- handoff триажа дополнен сдвигами в понимании и итогом тройного ревью.
- Проверка:
- утверждения по коду сверены с generation.py, runtime.py, state.py и
sql/ddl/dds/30_dds.sql двумя независимыми ревью-агентами.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+105
-19
@@ -1,4 +1,4 @@
|
|||||||
Status: needs-triage
|
Status: ready-for-agent
|
||||||
|
|
||||||
# Браузерная фактура не сохраняется на стыке восстановления визита
|
# Браузерная фактура не сохраняется на стыке восстановления визита
|
||||||
|
|
||||||
@@ -9,7 +9,8 @@ Status: needs-triage
|
|||||||
## Что нашли
|
## Что нашли
|
||||||
|
|
||||||
Визит, переживший стык backfill->live (активен на `T_end` и продолжается живьём),
|
Визит, переживший стык backfill->live (активен на `T_end` и продолжается живьём),
|
||||||
меняет браузерную фактуру в live-продолжении. Внутри одного `click_id` человек как
|
меняет браузерную фактуру в live-продолжении (фактура — конкретные значения полей
|
||||||
|
события: какой `browser_name`, user agent, язык стоят в строках). Внутри одного `click_id` человек как
|
||||||
будто «пересел» с одного браузера на другой посреди сессии.
|
будто «пересел» с одного браузера на другой посреди сессии.
|
||||||
|
|
||||||
Эмпирика. Чистый стенд, 2 суток backfill + 6 live-тиков за `T_end`,
|
Эмпирика. Чистый стенд, 2 суток backfill + 6 live-тиков за `T_end`,
|
||||||
@@ -37,16 +38,39 @@ Status: needs-triage
|
|||||||
внешний review gate другой родословной после задачи 5 — а он был выполнен той же
|
внешний review gate другой родословной после задачи 5 — а он был выполнен той же
|
||||||
родословной, и дефект прошёл.
|
родословной, и дефект прошёл.
|
||||||
|
|
||||||
## Причина (гипотеза по коду)
|
## Причина (подтверждена чтением кода, 2026-07-04)
|
||||||
|
|
||||||
Конкретные браузерные строки визита (`ActiveVisit.batch`) в state не сериализуются:
|
Первоначальная гипотеза «рассинхрон `event_index`» не подтвердилась: восстановление
|
||||||
хранятся `seed_click_id`, `page_url_paths`, `timestamps`, `next_index` (`state.py`).
|
перебирает визит с нуля, индекс события выровнен правильно (точнее, берётся
|
||||||
При восстановлении live-путь пересобирает фактуру
|
`event_index % len` — по модулю длины списка, см. `runtime.py:332`). Разошёлся сам
|
||||||
(`runtime.py._compact_visit_batch`: `browser_by_click_id[seed_click_id]`, индекс
|
**источник** фактуры:
|
||||||
`event_index % len`). device и geo переживают восстановление, потому что привязаны к
|
|
||||||
`UserProfile` (`user.device`, `user.geo`) и не зависят от индекса события; браузер
|
- При рождении визита (`generation.py`, `generate_batch`) браузерные строки берутся
|
||||||
зависит от выравнивания `event_index` между уже выпущенной частью и восстановленной —
|
у **случайного** click_id из словаря — «донора» (`rng.choice(visit_candidates)`,
|
||||||
вероятен рассинхрон индекса. Точную точку уточнить при фиксе.
|
затем `browser_by_click_id[base_click_id]`). Кто донор — нигде не сохраняется,
|
||||||
|
он живёт только в памяти процесса.
|
||||||
|
- При восстановлении из state (`runtime.py`, `_compact_visit_batch`) фактура
|
||||||
|
пересобирается из **другого** click_id — того, к которому привязан пользователь
|
||||||
|
(`browser_by_click_id[user.seed_click_id]`).
|
||||||
|
|
||||||
|
Донор и seed — разные click_id, отсюда разные браузеры. Внутри одного click_id
|
||||||
|
сида браузер практически постоянен (реальный клик одного человека), поэтому
|
||||||
|
«каждая сторона внутренне постоянна». `browser_language` разошёлся у 22/22
|
||||||
|
(случайные click_id почти всегда с разными языками), `browser_name` — у 17/22
|
||||||
|
(популярные браузеры иногда совпадают случайно). device и geo не расходятся,
|
||||||
|
потому что оба пути берут их из `UserProfile`.
|
||||||
|
|
||||||
|
Расхождение шире, чем браузер (уточнение по ревью 2026-07-04). Шаблон location
|
||||||
|
ищется по event_id браузерной строки (`location_by_event_id[...]` в обоих путях),
|
||||||
|
то есть тоже зависит от того, чей click_id взят источником. Из state
|
||||||
|
восстанавливаются только `page_url` / `page_url_path`; остальные per-event поля
|
||||||
|
location — `referer_url`, `referer_medium`, `utm_*` — приходят от источника и на
|
||||||
|
стыке расходятся так же, как браузер. Эмпирика выше их просто не замеряла.
|
||||||
|
|
||||||
|
Тот же `restore_state` -> `_visit_from_state` -> `_compact_visit_batch` работает и
|
||||||
|
при восстановлении после сбоя (задача 04) — дефект касается и этого пути, не
|
||||||
|
только стыка backfill->live. Стык и рестарт на уровне генератора — один и тот же
|
||||||
|
код, различие только в отбрасывании просроченных визитов.
|
||||||
|
|
||||||
## Масштаб и важность
|
## Масштаб и важность
|
||||||
|
|
||||||
@@ -58,15 +82,77 @@ Status: needs-triage
|
|||||||
- Для учебного демо урон низкий, но это реальное нарушение стыковой однородности и
|
- Для учебного демо урон низкий, но это реальное нарушение стыковой однородности и
|
||||||
сигнал, что браузерная фактура не входит в восстановимое состояние визита.
|
сигнал, что браузерная фактура не входит в восстановимое состояние визита.
|
||||||
|
|
||||||
## Направление фикса
|
## Решение по направлению фикса (пользователь, 2026-07-04)
|
||||||
|
|
||||||
- Либо сериализовать браузерную фактуру визита в state рядом с
|
**Хранить донора в state.** В сериализацию визита (`_visit_to_state`) добавить поле
|
||||||
`page_url_paths`/`timestamps`, чтобы восстановленная часть точно продолжила
|
с click_id донора фактуры (`base_click_id`), рядом с `page_url_paths` /
|
||||||
выпущенную.
|
`timestamps`. При восстановлении `_compact_visit_batch` берёт строки из
|
||||||
- Либо выбирать браузер по абсолютному `event_index` визита, а не по позиции в
|
`browser_by_click_id[base_click_id]` — по уже существующему абсолютному индексу
|
||||||
остатке, чтобы restore воспроизводил те же строки.
|
восстановленная часть точно продолжает выпущенную.
|
||||||
- В проверку стыка добавить per-event браузерные поля (`dds.event`), а не только
|
|
||||||
`dds.click`, чтобы регресс ловился автоматически.
|
Рассмотренные альтернативы (отклонены):
|
||||||
|
|
||||||
|
- Брать браузер от `seed_click_id` пользователя уже при рождении визита — schema
|
||||||
|
state не меняется и семантика красивее («один человек — один браузер»), но
|
||||||
|
меняется весь выпуск генератора: контрольные суммы, распределение браузеров,
|
||||||
|
артефакты пришлось бы перегенерировать. Это уже не багфикс, а смена поведения;
|
||||||
|
если захочется — отдельная задача.
|
||||||
|
- Сериализовать браузерные строки визита целиком — надёжно, но раздувает state и
|
||||||
|
дублирует словарь. Перебор: донор + абсолютный индекс дают тот же результат.
|
||||||
|
|
||||||
|
Детали для исполнителя:
|
||||||
|
|
||||||
|
- В основной ветке рождения донор покрывает визит целиком: кандидаты фильтруются
|
||||||
|
по `len(browser_events) >= len(visit_path)` (`generation.py:170`), так что
|
||||||
|
индекс не выйдет за список строк донора и `% len` становится безобидным
|
||||||
|
холостым ходом (оставить или убрать — на усмотрение при фиксе).
|
||||||
|
- **Запасная ветка рождения** (`generation.py:182-185`): если кандидатов нет,
|
||||||
|
берётся одна случайная браузерная строка и повторяется на весь визит — тогда
|
||||||
|
«восстановить по донору и индексу» даст другие строки. С реальным сидом ветка,
|
||||||
|
судя по фильтру, не срабатывает — проверить при фиксе и либо превратить её в
|
||||||
|
громкую ошибку, либо сериализовать так, чтобы восстановление её воспроизводило.
|
||||||
|
Молча оставить как есть нельзя.
|
||||||
|
- Неизвестный `base_click_id` при восстановлении — ошибка (по образцу проверки
|
||||||
|
`seed_click_id` в `_user_from_state`). Сейчас в `_compact_visit_batch` два
|
||||||
|
тихих fallback'а (`.get(...)` со словарём целиком для браузера и
|
||||||
|
`location_events[0]` для location) — для донора их заменить на ошибку, не
|
||||||
|
копировать.
|
||||||
|
- **Известное расхождение вне скоупа**: event_id при рождении — случайный uuid4
|
||||||
|
(`generation.py`, `_new_uuid`), при восстановлении — детерминированный uuid5 от
|
||||||
|
`click_id:index` (`runtime.py:140-141`). Фикс это не трогает: коллизий нет,
|
||||||
|
дублей не создаёт. Тест поэтому сравнивает события по всем полям, **кроме
|
||||||
|
`event_id`** — иначе он падает не из-за нашего бага.
|
||||||
|
- Расширение схемы state — поднять `STATE_VERSION` (`state.py`) и валидацию
|
||||||
|
нового поля. Старые state становятся несовместимы: поведение при этом — по
|
||||||
|
действующему правилу (сейчас — предупреждение и чистый старт; громкий отказ
|
||||||
|
при намерении продолжить делает задача 07, здесь его не реализовывать).
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [ ] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state
|
||||||
|
посреди визита, восстановить и сверить оставшиеся события с продолжением без
|
||||||
|
рестарта — по **всем** per-event полям, кроме `event_id` (браузерные и
|
||||||
|
location: referer, utm — не только browser_name; см. «Расхождение шире» выше).
|
||||||
|
Это один код восстановления для стыка backfill->live и crash-recovery
|
||||||
|
(задача 04) — одного теста на него достаточно.
|
||||||
|
- [ ] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что
|
||||||
|
нашли» (2 суток backfill + live за `T_end`, `GEN_SEED=4242`) визиты через стык
|
||||||
|
однородны в `dds.event` по браузерным полям (`browser_name`,
|
||||||
|
`browser_language`) **и** полям источника перехода (referer, utm) — 0
|
||||||
|
расхождений из `crossing_visits`. Запрос/скрипт проверки сохранён как
|
||||||
|
повторяемый, а не разовый.
|
||||||
|
- [ ] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов
|
||||||
|
device / os / geo на уровне ODS по-прежнему 0.
|
||||||
|
- [ ] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый
|
||||||
|
state обрабатывается по действующему правилу (см. детали выше).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Рекомендуемый режим ревью по coordinator-loop: **гейт** (state, сериализация —
|
||||||
|
из порогов риска). Ровно эта слепая зона уже пропустила дефект один раз:
|
||||||
|
критерий задач 04/05 проверяли только по `dds.click`.
|
||||||
|
- Задача 13 (доливка) идёт после этого фикса — restore-механизм должен быть
|
||||||
|
исправлен до того, как на него навешивать доливку.
|
||||||
|
|
||||||
## Blocked by
|
## Blocked by
|
||||||
|
|
||||||
|
|||||||
+61
-19
@@ -1,4 +1,4 @@
|
|||||||
Status: needs-triage
|
Status: ready-for-agent
|
||||||
|
|
||||||
# Гео-карта на дашборде нечитаема
|
# Гео-карта на дашборде нечитаема
|
||||||
|
|
||||||
@@ -9,39 +9,81 @@ Status: needs-triage
|
|||||||
|
|
||||||
## Что нашли
|
## Что нашли
|
||||||
|
|
||||||
На финальной ручной приёмке (шаг 2 спеки) виджет «🗺️ Geography Map» показывает
|
На финальной ручной приёмке (шаг 2 спеки) виджет «🌍 Geography Map» показывает
|
||||||
что-то, но прочитать нельзя:
|
что-то, но прочитать нельзя:
|
||||||
|
|
||||||
- нет всплывающих подсказок (tooltip) — навёл на страну, значения не видно;
|
- нет всплывающих подсказок (tooltip) — навёл на страну, значения не видно;
|
||||||
- нет легенды и подписи шкалы — непонятно, что кодирует цвет и в каких единицах;
|
- нет легенды и подписи шкалы — непонятно, что кодирует цвет и в каких единицах;
|
||||||
- цветовая шкала неочевидна — подсвечено мало стран, остальное пусто.
|
- цветовая шкала неочевидна — подсвечено мало стран, остальное пусто.
|
||||||
|
|
||||||
Виз отрисовывается, но смысл непрозрачен: нельзя сказать, что именно он показывает.
|
График рисуется, но непонятно, что именно он показывает.
|
||||||
|
|
||||||
## Две разные причины (не путать)
|
## Две разные причины (не путать)
|
||||||
|
|
||||||
1. **Читаемость визуализации (эта задача).** Настройка чарта: включить tooltip,
|
1. **Читаемость визуализации (эта задача).** Сделать так, чтобы у графика были
|
||||||
добавить легенду и подпись метрики/единиц, выбрать понятную цветовую шкалу,
|
tooltip, легенда и подпись метрики/единиц и понятная цветовая шкала — либо
|
||||||
при необходимости — заменить мировую choropleth на более читаемую форму
|
настройкой текущего чарта, либо заменой карты (choropleth — карта, где страна
|
||||||
(например, топ-N стран баром), раз распределение сильно разрежено.
|
закрашена по значению) на более читаемую форму, например топ-N стран
|
||||||
2. **Перекос гео-данных (отдельно, ADR-0006).** Сама гео-фактура берётся из
|
столбцами, потому что распределение сильно разрежено. Важно: сейчас это
|
||||||
статического сида (`geo_by_click_id`) с перекошенным распределением стран. Это
|
legacy-виз `world_map` (`superset/create_dashboard.py:149-164`), и у него,
|
||||||
«временная опора на сид» до синтеза фактуры — лечится не настройкой чарта, а
|
судя по параметрам, легенда и tooltip вообще не настраиваются — скорее всего,
|
||||||
генерацией гео. Здесь не решаем, только отмечаем как смежную причину.
|
основной путь именно смена типа визуализации, а не подкрутка текущего.
|
||||||
|
Возможности виз-типов уточнить по актуальной документации Superset через
|
||||||
|
Context7 (правило AGENTS.md) и зафиксировать выбор.
|
||||||
|
2. **Перекос гео-данных (отдельно).** Сама гео-фактура (значения стран в
|
||||||
|
событиях) берётся из статического сида (`geo_by_click_id`), и на приёмке
|
||||||
|
распределение стран оказалось сильно перекошенным — закрашено мало стран.
|
||||||
|
Опора на чужой сид — осознанно временная (ADR-0006: своя гео-фактура —
|
||||||
|
отдельный будущий шаг); лечится это генерацией гео, а не настройкой чарта.
|
||||||
|
Здесь не решаем, только отмечаем как смежную причину.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] У гео-виза есть tooltip со значением по стране.
|
- [ ] У гео-графика есть tooltip со значением по стране (на выбранном типе
|
||||||
- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется цветом.
|
визуализации — карте или замене).
|
||||||
- [ ] Цветовая шкала читаема на текущем (разреженном) распределении, либо выбран
|
- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется
|
||||||
более подходящий тип визуализации.
|
(для текущей метрики `COUNT(*)` честный ответ — «событий, штук»).
|
||||||
- [ ] Зафиксировано, что перекос распределения стран — это вопрос гео-фактуры
|
- [ ] Выбранный тип визуализации читаем на текущем (разреженном) распределении;
|
||||||
(ADR-0006), а не настройки чарта.
|
выбор типа (оставить карту или заменить) зафиксирован с коротким «почему».
|
||||||
|
- [ ] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры
|
||||||
|
из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки
|
||||||
|
чарта.
|
||||||
|
- [ ] Приёмка пройдена по скриншотам (см. «Как принимать» ниже), скриншот «после»
|
||||||
|
приложен к итогам задачи.
|
||||||
|
|
||||||
|
## Как принимать (дописано 2026-07-04)
|
||||||
|
|
||||||
|
Проверка визуальная, через скриншоты playwright-cli (так уже делали в спеке
|
||||||
|
редизайна дашборда `docs/specs/2026-06-06-superset-dashboard-redesign.md`):
|
||||||
|
|
||||||
|
- Скриншоты «до» и «после»: кадр виджета целиком, плюс кадр с наведением курсора
|
||||||
|
на закрашенную страну / столбец (виден tooltip).
|
||||||
|
- «Читаемо» значит: по кадру виджета целиком (без обращения к SQL и коду чарта)
|
||||||
|
можно ответить на три вопроса — какая метрика показана, в каких единицах, у
|
||||||
|
какой страны значение больше. Если по кадру ответить нельзя — не принято.
|
||||||
|
- Tooltip — по отдельному кадру с наведением: видно название страны и значение
|
||||||
|
метрики. Кадр с tooltip в headless-прогоне может стабильно не ловиться
|
||||||
|
(tooltip следует за курсором); тогда допустим другой способ показать tooltip
|
||||||
|
(например, короткая запись экрана) — согласовать с человеком, молча не
|
||||||
|
ослаблять.
|
||||||
|
|
||||||
|
Состояние стенда: стенд не трогали с 2026-06-14 и он мог умереть. Это нормально:
|
||||||
|
полная пересборка (`make generated-history-analytics`) заново генерирует backfill.
|
||||||
|
По умолчанию она даёт **6 часов** модельной истории
|
||||||
|
(`GEN_MODEL_T_END` в `scripts/run_generated_history_analytics.sh`) — для приёмки
|
||||||
|
читаемости этого достаточно. Если нужен вид как на исходной находке (2 суток),
|
||||||
|
переопределить `GEN_MODEL_T_END` на `T0`+2 суток при запуске; профиль «2 суток
|
||||||
|
одной командой» — это ещё не сделанная задача 11, здесь её не делать.
|
||||||
|
Портативный артефакт (задача 07) для этой задачи не нужен и не блокирует её.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Всплыло на просмотре дашборда на данных генерации (2 суток backfill).
|
- Всплыло на просмотре дашборда на данных генерации (2 суток backfill).
|
||||||
- Смежные документы: `superset/create_dashboard.py` (определение чарта),
|
- Смежные документы: `superset/create_dashboard.py` (определение чарта),
|
||||||
`docs/SUPERSET_DASHBOARD.md`.
|
`docs/SUPERSET_DASHBOARD.md` — при смене типа визуализации обновить в этом же
|
||||||
|
изменении.
|
||||||
- Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и
|
- Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и
|
||||||
подсказки у других виджетов), но базово задача — про гео-карту.
|
подсказки у других виджетов), но базово задача — про гео-карту. Если проход
|
||||||
|
делается, он не должен раздувать задачу: заметил — запиши отдельным issue.
|
||||||
|
- Рекомендуемый режим ревью по coordinator-loop: обычный (риска для state и
|
||||||
|
данных нет, правка ограничена определением чарта и документацией).
|
||||||
|
|||||||
@@ -43,20 +43,34 @@
|
|||||||
|
|
||||||
## Следующий шаг
|
## Следующий шаг
|
||||||
|
|
||||||
Дооформить до `ready-for-agent` два независимых issue (можно параллельно с
|
Весь бэклог готов к передаче Codex: `/goal` на отдельные задачи (09 и 10
|
||||||
остальной очередью):
|
независимы, можно параллельно) или `/coordinator-loop` на цепочки 07 -> 08 и
|
||||||
|
07 -> 11 -> 12. Задача 13 — после фикса 09.
|
||||||
|
|
||||||
1. **09 — баг браузерной фактуры на стыке.** Нет секции Acceptance criteria;
|
## Дооформление 09 и 10 — что изменилось против прежнего понимания
|
||||||
зафиксировать выбор направления фикса (сериализация фактуры в state против
|
|
||||||
выбора по абсолютному `event_index`; второе не трогает схему state) и
|
|
||||||
требование теста по per-event полям `dds.event`, включая crash-recovery
|
|
||||||
(гипотеза: тот же restore-механизм). Режим ревью — гейт (state).
|
|
||||||
2. **10 — гео-карта.** Прописать способ приёмки (Playwright-скриншоты, что
|
|
||||||
считать «читаемо»); учесть, что стенд с 14 июня мог умереть, а полная
|
|
||||||
пересборка регенерирует backfill.
|
|
||||||
|
|
||||||
После этого весь бэклог готов к передаче Codex (`/goal` на задачу или
|
Сделано в этот же день, вторым заходом. 09 и 10 переведены в `ready-for-agent`;
|
||||||
`/coordinator-loop` на цепочку 07 -> 08 / 07 -> 11 -> 12).
|
детали — в самих issue, здесь только сдвиги в понимании:
|
||||||
|
|
||||||
|
- **Гипотеза по 09 не подтвердилась.** Дело не в рассинхроне `event_index`, а в
|
||||||
|
источнике фактуры: при рождении визита браузерные строки берутся у случайного
|
||||||
|
«донора» из словаря, при восстановлении — у `seed_click_id` пользователя.
|
||||||
|
Прежняя развилка «выбор по абсолютному индексу, не трогая схему state»
|
||||||
|
оказалась нерабочей — restore не знает донора. Решение пользователя: хранить
|
||||||
|
`base_click_id` донора в state (альтернативы и причины отказа — в issue).
|
||||||
|
- **Дефект 09 шире браузера** (нашло ревью): per-event поля location (referer,
|
||||||
|
utm) расходятся на стыке так же — критерии приёмки расширены, иначе частичный
|
||||||
|
фикс «только браузер» прошёл бы приёмку. Расхождение event_id (uuid4 при
|
||||||
|
рождении, uuid5 при restore) зафиксировано как известное и вне скоупа.
|
||||||
|
- **По 10 сняты две мины:** пересборка по умолчанию даёт 6 часов истории, а не
|
||||||
|
2 суток (профиль «2 суток одной командой» — это ещё не сделанная задача 11);
|
||||||
|
legacy-виз `world_map`, похоже, вообще не умеет легенду и tooltip — основной
|
||||||
|
путь, вероятно, смена типа визуализации, а не настройка (проверить через
|
||||||
|
Context7 при реализации).
|
||||||
|
- **Процессная заметка:** дооформленные issue прогнаны через тройное ревью
|
||||||
|
(самопроверка + два свежих агента, адверсарно, с проверкой каждого утверждения
|
||||||
|
по коду). Улов оправдал затраты — см. пункты выше; для issue с режимом «гейт»
|
||||||
|
так стоит делать и дальше.
|
||||||
|
|
||||||
## Не забыть (вне бэклога)
|
## Не забыть (вне бэклога)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user