mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 07:06:32 +00:00
Fixed dynamic ONNX input
Fix dynamic ONNX input Update docs with correct file name for tests
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
# Codex Context Bridge
|
||||||
|
|
||||||
|
This file is a compact compatibility layer for Codex. It explains how the repository uses `.cursor/`, where project memory lives, and what should be read first in a new chat.
|
||||||
|
|
||||||
|
## First Read Order
|
||||||
|
|
||||||
|
When starting a new Codex session in this repository, read in this order:
|
||||||
|
|
||||||
|
1. `AGENTS.md`
|
||||||
|
2. `.cursor/CODEX_CONTEXT.md`
|
||||||
|
3. `_docs/_autopilot_state.md`
|
||||||
|
4. The skill file relevant to the user's request under `.cursor/skills/*/SKILL.md`
|
||||||
|
5. Only the `_docs/` artifacts and `.cursor/rules/*.mdc` files relevant to that request
|
||||||
|
|
||||||
|
Do not bulk-read all of `_docs/` or all skill files unless the task truly needs it.
|
||||||
|
|
||||||
|
## Mental Model
|
||||||
|
|
||||||
|
- `.cursor/` is the workflow engine, policy layer, and skill library
|
||||||
|
- `_docs/` is the persisted working memory for the project
|
||||||
|
- `src/`, `tests/`, `e2e/`, and related runtime files are the implementation layer
|
||||||
|
|
||||||
|
For Codex, the important distinction is:
|
||||||
|
|
||||||
|
- `.cursor/` tells you **how the team wants work to happen**
|
||||||
|
- `_docs/` tells you **what has already been decided or completed**
|
||||||
|
|
||||||
|
## Project Snapshot
|
||||||
|
|
||||||
|
- Product: `Azaion.Detections`
|
||||||
|
- Type: Python/Cython microservice for aerial object detection
|
||||||
|
- API: FastAPI + SSE
|
||||||
|
- Engines: TensorRT on compatible NVIDIA GPUs, ONNX Runtime fallback
|
||||||
|
- Main code areas: `src/`, `tests/`, `e2e/`, `scripts/`
|
||||||
|
- Workflow memory: `_docs/`
|
||||||
|
|
||||||
|
Relevant documented architecture:
|
||||||
|
|
||||||
|
- 4 components: Domain, Inference Engines, Inference Pipeline, API
|
||||||
|
- 10 documented modules under `_docs/02_document/modules/`
|
||||||
|
- External services: Loader service, Annotations service
|
||||||
|
|
||||||
|
## Current Workflow State
|
||||||
|
|
||||||
|
As of `2026-04-15`, the persisted workflow state says:
|
||||||
|
|
||||||
|
- Flow: `existing-code`
|
||||||
|
- Current step: `2`
|
||||||
|
- Current step name: `Test Spec`
|
||||||
|
- Current status: `in_progress`
|
||||||
|
- Current sub-step: `Phase 3 - Test Data Validation Gate`
|
||||||
|
|
||||||
|
Important rollback note from `_docs/_autopilot_state.md`:
|
||||||
|
|
||||||
|
- On `2026-04-10`, the workflow was rolled back from Step 8 (`New Task`) to Step 2 (`Test Spec`)
|
||||||
|
- Reason: expected-result artifacts were incomplete for verification
|
||||||
|
|
||||||
|
Concrete blocker confirmed from current files:
|
||||||
|
|
||||||
|
- `_docs/00_problem/input_data/expected_results/results_report.md` still contains `?` for most expected detection counts
|
||||||
|
- Per-file expected-result CSVs for non-empty datasets are header-only
|
||||||
|
- That means black-box tests cannot verify detection correctness yet
|
||||||
|
|
||||||
|
Practical unblocker:
|
||||||
|
|
||||||
|
1. Populate the expected-result CSVs for the non-empty image/video fixtures
|
||||||
|
2. Replace `?` counts in `results_report.md` with real values
|
||||||
|
3. Re-run or continue the `test-spec` workflow from Phase 3
|
||||||
|
|
||||||
|
## Cursor Asset Map
|
||||||
|
|
||||||
|
### Core entry points
|
||||||
|
|
||||||
|
- `.cursor/README.md`: high-level overview of the whole Cursor system
|
||||||
|
- `.cursor/skills/autopilot/SKILL.md`: orchestrator contract
|
||||||
|
- `.cursor/skills/autopilot/flows/existing-code.md`: active flow for this repository
|
||||||
|
- `.cursor/skills/autopilot/protocols.md`: decision, retry, and re-entry rules
|
||||||
|
- `.cursor/agents/implementer.md`: only defined subagent
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
Always-check rules:
|
||||||
|
|
||||||
|
- `.cursor/rules/meta-rule.mdc`
|
||||||
|
- `.cursor/rules/techstackrule.mdc`
|
||||||
|
- `.cursor/rules/git-workflow.mdc`
|
||||||
|
- `.cursor/rules/quality-gates.mdc`
|
||||||
|
|
||||||
|
Highly relevant contextual rules for this repo:
|
||||||
|
|
||||||
|
- `.cursor/rules/python.mdc`
|
||||||
|
- `.cursor/rules/testing.mdc`
|
||||||
|
- `.cursor/rules/docker.mdc`
|
||||||
|
- `.cursor/rules/cursor-meta.mdc`
|
||||||
|
|
||||||
|
Other rules exist for security, trackers, OpenAPI, React, Rust, SQL, and .NET. Read them only if the task touches those domains.
|
||||||
|
|
||||||
|
## Skills Index
|
||||||
|
|
||||||
|
Use this table as the fast map instead of opening every skill up front.
|
||||||
|
|
||||||
|
| Skill | Primary use | Main outputs |
|
||||||
|
|------|-------------|--------------|
|
||||||
|
| `autopilot` | Continue the end-to-end workflow | state progression in `_docs/_autopilot_state.md` |
|
||||||
|
| `problem` | Gather or refine the problem definition | `_docs/00_problem/` |
|
||||||
|
| `research` | Investigate solutions or unknowns | `_docs/01_solution/` or standalone research folder |
|
||||||
|
| `plan` | Architecture, components, risks, tests, epics | `_docs/02_document/` |
|
||||||
|
| `test-spec` | Black-box test specifications and test runners | `_docs/02_document/tests/`, `scripts/run-tests.sh`, `scripts/run-performance-tests.sh` |
|
||||||
|
| `decompose` | Break plan or tests into atomic tasks | `_docs/02_tasks/` |
|
||||||
|
| `implement` | Batch orchestration of coding tasks | `_docs/03_implementation/` plus code changes |
|
||||||
|
| `test-run` | Execute and diagnose test suites | test results and pass/fail guidance |
|
||||||
|
| `code-review` | Review implemented batches against specs | review report and verdict |
|
||||||
|
| `new-task` | Plan new functionality for existing code | `_docs/02_tasks/todo/` and optional `_docs/02_task_plans/` |
|
||||||
|
| `refactor` | Structured refactoring with safety checks | `_docs/04_refactoring/` |
|
||||||
|
| `security` | Security audit and OWASP-style review | `_docs/05_security/` |
|
||||||
|
| `document` | Reverse-engineer or update docs from code | `_docs/02_document/` and related problem/solution docs |
|
||||||
|
| `deploy` | Containerization, CI/CD, observability | `_docs/04_deploy/` |
|
||||||
|
| `retrospective` | Review implementation metrics and trends | `_docs/06_metrics/` |
|
||||||
|
| `ui-design` | UI mockups and design system artifacts | `_docs/02_document/ui_mockups/` |
|
||||||
|
|
||||||
|
## Agents
|
||||||
|
|
||||||
|
Defined agent:
|
||||||
|
|
||||||
|
- `implementer`
|
||||||
|
- File: `.cursor/agents/implementer.md`
|
||||||
|
- Role: implement one task spec with tests and AC verification
|
||||||
|
- Invoked by: `implement` skill
|
||||||
|
|
||||||
|
No other `.cursor/agents/` definitions are currently present.
|
||||||
|
|
||||||
|
## Codex Operating Notes
|
||||||
|
|
||||||
|
### When the user asks for Cursor-style continuation
|
||||||
|
|
||||||
|
If the user says things like:
|
||||||
|
|
||||||
|
- "continue autopilot"
|
||||||
|
- "what's next"
|
||||||
|
- "continue workflow"
|
||||||
|
- "/autopilot"
|
||||||
|
|
||||||
|
then:
|
||||||
|
|
||||||
|
1. Read `_docs/_autopilot_state.md`
|
||||||
|
2. Read `.cursor/skills/autopilot/SKILL.md`
|
||||||
|
3. Read `.cursor/skills/autopilot/protocols.md`
|
||||||
|
4. Read the active flow file
|
||||||
|
5. Read only the specific downstream skill file needed for the current step
|
||||||
|
|
||||||
|
### When the user asks for direct coding help
|
||||||
|
|
||||||
|
You do not need to force the full Cursor workflow. Work directly in the codebase, but still:
|
||||||
|
|
||||||
|
- respect `.cursor/rules/*.mdc`
|
||||||
|
- use `_docs/` as authoritative project memory
|
||||||
|
- preserve alignment with existing task specs and documented architecture when relevant
|
||||||
|
|
||||||
|
### Context discipline
|
||||||
|
|
||||||
|
- Prefer progressive loading over reading everything
|
||||||
|
- Treat disk artifacts as the source of truth, not prior chat history
|
||||||
|
- Cross-check state file claims against actual files when something seems inconsistent
|
||||||
|
|
||||||
|
## Most Relevant Files For This Repo
|
||||||
|
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `.cursor/CODEX_CONTEXT.md`
|
||||||
|
- `.cursor/README.md`
|
||||||
|
- `.cursor/skills/autopilot/SKILL.md`
|
||||||
|
- `.cursor/skills/autopilot/flows/existing-code.md`
|
||||||
|
- `.cursor/skills/test-spec/SKILL.md`
|
||||||
|
- `.cursor/agents/implementer.md`
|
||||||
|
- `_docs/_autopilot_state.md`
|
||||||
|
- `_docs/00_problem/`
|
||||||
|
- `_docs/01_solution/solution.md`
|
||||||
|
- `_docs/02_document/`
|
||||||
|
- `_docs/02_tasks/`
|
||||||
|
|
||||||
|
## Short Version
|
||||||
|
|
||||||
|
If you only have a minute:
|
||||||
|
|
||||||
|
- This repo uses Cursor as a workflow framework and `_docs/` as persistent memory
|
||||||
|
- The project is already documented and mid-workflow
|
||||||
|
- The current workflow is blocked in `test-spec` because expected-result data is incomplete
|
||||||
|
- For future Codex chats, start with `AGENTS.md`, this file, and `_docs/_autopilot_state.md`
|
||||||
@@ -80,3 +80,6 @@ data/
|
|||||||
|
|
||||||
# Runtime logs
|
# Runtime logs
|
||||||
Logs/
|
Logs/
|
||||||
|
|
||||||
|
#IDEA
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ For videos, the additional field:
|
|||||||
|
|
||||||
| # | Input File | Description | Expected Result File | Notes |
|
| # | Input File | Description | Expected Result File | Notes |
|
||||||
|---|------------|-------------|---------------------|-------|
|
|---|------------|-------------|---------------------|-------|
|
||||||
| 7 | `video_short01.mp4` | Standard test video | `video_short01_expected.csv` | Primary async/SSE/video test. List key-frame detections. |
|
| 7 | `video_test01.mp4` | Standard test video | `video_test01_expected.csv` | Primary async/SSE/video test. List key-frame detections. |
|
||||||
| 8 | `video_short02.mp4` | Video variant | `video_short02_expected.csv` | Used for resilience and concurrent tests |
|
| 8 | `video_1.mp4` | Video variant | `video_1_expected.csv` | Secondary local fixture for resilience and concurrent-style validation. |
|
||||||
| 9 | `video_long03.mp4` | Long video (288MB), generates >100 SSE events | `video_long03_expected.csv` | SSE overflow test. Only key-frame samples needed. |
|
| 9 | `video_1_faststart.mp4` | Faststart video variant | `video_1_faststart_expected.csv` | Streaming compatibility variant. Separate long-video overflow fixture is not currently present in local fixtures. |
|
||||||
|
|
||||||
## How to Fill
|
## How to Fill
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
| image-dense-02 | `input_data/image_dense02.jpg` | JPEG 1920×1080 — dense scene variant, borderline tiling | FT-P-06 (variant) | Volume mount to consumer `/media/` | N/A (read-only) |
|
| image-dense-02 | `input_data/image_dense02.jpg` | JPEG 1920×1080 — dense scene variant, borderline tiling | FT-P-06 (variant) | Volume mount to consumer `/media/` | N/A (read-only) |
|
||||||
| image-different-types | `input_data/image_different_types.jpg` | JPEG 900×1600 — varied object classes for class variant tests | FT-P-13 | Volume mount to consumer `/media/` | N/A (read-only) |
|
| image-different-types | `input_data/image_different_types.jpg` | JPEG 900×1600 — varied object classes for class variant tests | FT-P-13 | Volume mount to consumer `/media/` | N/A (read-only) |
|
||||||
| image-empty-scene | `input_data/image_empty_scene.jpg` | JPEG 1920×1080 — clean scene with no detectable objects | Edge case (zero detections) | Volume mount to consumer `/media/` | N/A (read-only) |
|
| image-empty-scene | `input_data/image_empty_scene.jpg` | JPEG 1920×1080 — clean scene with no detectable objects | Edge case (zero detections) | Volume mount to consumer `/media/` | N/A (read-only) |
|
||||||
| video-short-01 | `input_data/video_short01.mp4` | MP4 video — standard async/SSE/video detection tests | FT-P-08..12, FT-N-04, 07, NFT-PERF-04, NFT-RES-02, NFT-SEC-03 | Volume mount to consumer `/media/` | N/A (read-only) |
|
| video-test-01 | `input_data/video_test01.mp4` | MP4 video — standard async/SSE/video detection tests | FT-P-08..12, FT-N-04, 07, NFT-PERF-04, NFT-RES-02, NFT-SEC-03 | Volume mount to consumer `/media/` | N/A (read-only) |
|
||||||
| video-short-02 | `input_data/video_short02.mp4` | MP4 video — variant for concurrent and resilience tests | NFT-RES-02 (variant), NFT-RES-04 | Volume mount to consumer `/media/` | N/A (read-only) |
|
| video-1 | `input_data/video_1.mp4` | MP4 video — local variant for concurrent and resilience-style tests | NFT-RES-02 (variant), NFT-RES-04 | Volume mount to consumer `/media/` | N/A (read-only) |
|
||||||
| video-long-03 | `input_data/video_long03.mp4` | MP4 long video (288MB) — generates >100 SSE events for overflow tests | FT-N-08, NFT-RES-LIM-02 | Volume mount to consumer `/media/` | N/A (read-only) |
|
| video-1-faststart | `input_data/video_1_faststart.mp4` | MP4 video — faststart/local streaming variant | Streaming compatibility checks | Volume mount to consumer `/media/` | N/A (read-only) |
|
||||||
| empty-image | Generated at build time | Zero-byte file | FT-N-01 | Generated in e2e/fixtures/ | N/A |
|
| empty-image | Generated at build time | Zero-byte file | FT-N-01 | Generated in e2e/fixtures/ | N/A |
|
||||||
| corrupt-image | Generated at build time | Random binary garbage (not valid image format) | FT-N-02 | Generated in e2e/fixtures/ | N/A |
|
| corrupt-image | Generated at build time | Random binary garbage (not valid image format) | FT-N-02 | Generated in e2e/fixtures/ | N/A |
|
||||||
| jwt-token | Generated at runtime | Valid JWT with exp claim (not signature-verified by detections) | FT-P-08, 09, FT-N-04, 07, NFT-SEC-03 | Generated by consumer at runtime | N/A |
|
| jwt-token | Generated at runtime | Valid JWT with exp claim (not signature-verified by detections) | FT-P-08, 09, FT-N-04, 07, NFT-SEC-03 | Generated by consumer at runtime | N/A |
|
||||||
@@ -35,9 +35,9 @@ Each test run starts with fresh containers (`docker compose down -v && docker co
|
|||||||
| image_dense02.jpg | `_docs/00_problem/input_data/image_dense02.jpg` | Dense scene 1920×1080 | Dedup variant |
|
| image_dense02.jpg | `_docs/00_problem/input_data/image_dense02.jpg` | Dense scene 1920×1080 | Dedup variant |
|
||||||
| image_different_types.jpg | `_docs/00_problem/input_data/image_different_types.jpg` | Varied classes 900×1600 | Class variant tests |
|
| image_different_types.jpg | `_docs/00_problem/input_data/image_different_types.jpg` | Varied classes 900×1600 | Class variant tests |
|
||||||
| image_empty_scene.jpg | `_docs/00_problem/input_data/image_empty_scene.jpg` | Empty scene 1920×1080 | Zero-detection edge case |
|
| image_empty_scene.jpg | `_docs/00_problem/input_data/image_empty_scene.jpg` | Empty scene 1920×1080 | Zero-detection edge case |
|
||||||
| video_short01.mp4 | `_docs/00_problem/input_data/video_short01.mp4` | Standard video | Async, SSE, video, perf tests |
|
| video_test01.mp4 | `_docs/00_problem/input_data/video_test01.mp4` | Standard video | Async, SSE, video, perf tests |
|
||||||
| video_short02.mp4 | `_docs/00_problem/input_data/video_short02.mp4` | Video variant | Resilience, concurrent tests |
|
| video_1.mp4 | `_docs/00_problem/input_data/video_1.mp4` | Video variant | Resilience, concurrent tests |
|
||||||
| video_long03.mp4 | `_docs/00_problem/input_data/video_long03.mp4` | Long video (288MB) | SSE overflow, queue depth tests |
|
| video_1_faststart.mp4 | `_docs/00_problem/input_data/video_1_faststart.mp4` | Faststart video variant | Streaming compatibility checks |
|
||||||
| classes.json | repo root `classes.json` | 19 detection classes | All tests |
|
| classes.json | repo root `classes.json` | 19 detection classes | All tests |
|
||||||
|
|
||||||
## External Dependency Mocks
|
## External Dependency Mocks
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
cimport constants_inf
|
||||||
from loader_http_client cimport LoaderHttpClient, LoadResult
|
from loader_http_client cimport LoaderHttpClient, LoadResult
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +43,6 @@ class EngineFactory:
|
|||||||
|
|
||||||
def build_and_cache(self, bytes source_bytes, LoaderHttpClient loader_client, str models_dir):
|
def build_and_cache(self, bytes source_bytes, LoaderHttpClient loader_client, str models_dir):
|
||||||
cdef LoadResult res
|
cdef LoadResult res
|
||||||
import constants_inf
|
|
||||||
engine_bytes, engine_filename = self.build_from_source(source_bytes, loader_client, models_dir)
|
engine_bytes, engine_filename = self.build_from_source(source_bytes, loader_client, models_dir)
|
||||||
res = loader_client.upload_big_small_resource(engine_bytes, engine_filename, models_dir)
|
res = loader_client.upload_big_small_resource(engine_bytes, engine_filename, models_dir)
|
||||||
if res.err is not None:
|
if res.err is not None:
|
||||||
@@ -56,7 +56,6 @@ class OnnxEngineFactory(EngineFactory):
|
|||||||
return OnnxEngine(model_bytes)
|
return OnnxEngine(model_bytes)
|
||||||
|
|
||||||
def get_source_filename(self):
|
def get_source_filename(self):
|
||||||
import constants_inf
|
|
||||||
return constants_inf.AI_ONNX_MODEL_FILE
|
return constants_inf.AI_ONNX_MODEL_FILE
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +80,6 @@ class TensorRTEngineFactory(EngineFactory):
|
|||||||
return TensorRTEngine.get_engine_filename()
|
return TensorRTEngine.get_engine_filename()
|
||||||
|
|
||||||
def get_source_filename(self):
|
def get_source_filename(self):
|
||||||
import constants_inf
|
|
||||||
return constants_inf.AI_ONNX_MODEL_FILE
|
return constants_inf.AI_ONNX_MODEL_FILE
|
||||||
|
|
||||||
def build_from_source(self, onnx_bytes, loader_client, models_dir):
|
def build_from_source(self, onnx_bytes, loader_client, models_dir):
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ cdef class OnnxEngine(InferenceEngine):
|
|||||||
cdef object model_inputs
|
cdef object model_inputs
|
||||||
cdef str input_name
|
cdef str input_name
|
||||||
cdef object input_shape
|
cdef object input_shape
|
||||||
|
cdef object _resolved_input_hw
|
||||||
|
|
||||||
|
cdef tuple _resolve_input_hw(self, object metadata)
|
||||||
cdef tuple get_input_shape(self)
|
cdef tuple get_input_shape(self)
|
||||||
cdef run(self, input_data)
|
cdef run(self, input_data)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from engines.inference_engine cimport InferenceEngine
|
|||||||
import onnxruntime as onnx
|
import onnxruntime as onnx
|
||||||
cimport constants_inf
|
cimport constants_inf
|
||||||
|
|
||||||
|
import ast
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def _select_providers():
|
def _select_providers():
|
||||||
@@ -29,15 +30,40 @@ cdef class OnnxEngine(InferenceEngine):
|
|||||||
model_meta = self.session.get_modelmeta()
|
model_meta = self.session.get_modelmeta()
|
||||||
constants_inf.log(f"Metadata: {model_meta.custom_metadata_map}")
|
constants_inf.log(f"Metadata: {model_meta.custom_metadata_map}")
|
||||||
|
|
||||||
|
self._resolved_input_hw = self._resolve_input_hw(model_meta.custom_metadata_map)
|
||||||
|
|
||||||
self._cpu_session = None
|
self._cpu_session = None
|
||||||
if any("CoreML" in p for p in self.session.get_providers()):
|
if any("CoreML" in p for p in self.session.get_providers()):
|
||||||
constants_inf.log(<str>'CoreML active — creating CPU fallback session')
|
constants_inf.log(<str>'CoreML active — creating CPU fallback session')
|
||||||
self._cpu_session = onnx.InferenceSession(
|
self._cpu_session = onnx.InferenceSession(
|
||||||
model_bytes, providers=["CPUExecutionProvider"])
|
model_bytes, providers=["CPUExecutionProvider"])
|
||||||
|
|
||||||
|
cdef tuple _resolve_input_hw(self, object metadata):
|
||||||
|
cdef object h = self.input_shape[2] if len(self.input_shape) > 2 else None
|
||||||
|
cdef object w = self.input_shape[3] if len(self.input_shape) > 3 else None
|
||||||
|
cdef int resolved_h
|
||||||
|
cdef int resolved_w
|
||||||
|
|
||||||
|
if isinstance(h, int) and h > 0 and isinstance(w, int) and w > 0:
|
||||||
|
return <tuple>(h, w)
|
||||||
|
|
||||||
|
try:
|
||||||
|
imgsz = metadata.get("imgsz") if metadata is not None else None
|
||||||
|
if imgsz:
|
||||||
|
parsed = ast.literal_eval(imgsz)
|
||||||
|
if isinstance(parsed, (list, tuple)) and len(parsed) == 2:
|
||||||
|
resolved_h = int(parsed[0])
|
||||||
|
resolved_w = int(parsed[1])
|
||||||
|
if resolved_h > 0 and resolved_w > 0:
|
||||||
|
return <tuple>(resolved_h, resolved_w)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Dynamic ONNX models are expected to use the project's canonical 1280x1280 input.
|
||||||
|
return <tuple>(1280, 1280)
|
||||||
|
|
||||||
cdef tuple get_input_shape(self):
|
cdef tuple get_input_shape(self):
|
||||||
shape = self.input_shape
|
return self._resolved_input_hw
|
||||||
return <tuple>(shape[2], shape[3])
|
|
||||||
|
|
||||||
cdef run(self, input_data):
|
cdef run(self, input_data):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user