chore: replace AI skill library with 16 verified mvp-* skills
Deploy to Staging / Build Images (push) Successful in 5m32s
Deploy to Staging / Deploy to Staging (push) Successful in 44s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Deploy to Staging / Build Images (push) Successful in 5m32s
Deploy to Staging / Deploy to Staging (push) Successful in 44s
Deploy to Staging / Verify Staging (push) Successful in 5s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Removes the old planner/decision-critic/role-agents/domain-agents system (step-injector scripts, stale scopes, sprint-era workflow) and replaces it with 16 ground-truth-verified skills under .claude/skills/: change control, debugging playbook, failure archaeology, architecture contract, domain reference, OCR/Gemini pipeline, config and secrets, build and env, run and operate, diagnostics (with tested scripts), validation and QA, docs and writing, launch readiness, deploy-safety campaign, proof and analysis toolkit, and research frontier. RULE 0/1/2, the temporal-contamination rule, and the decision stress-test protocol are carried forward into the new skills; the retired content remains in git history. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e729d425fd
commit
c239bb9347
@@ -0,0 +1,219 @@
|
||||
---
|
||||
name: mvp-vehicle-domain-reference
|
||||
description: >-
|
||||
Vehicle domain theory as implemented in MotoVaultPro. Load when working on VIN
|
||||
validation, VIN decode, check digits, model-year codes, "wrong year decoded", OCR VIN
|
||||
misreads (I/O/Q), fuel efficiency math, MPG or km/L calculations, unit conversion,
|
||||
imperial/metric, fuel grades, DATE vs TIMESTAMP column semantics, subscription tiers,
|
||||
vehicle limits, pro-gated features, or the year/make/model/trim dropdown catalog. This
|
||||
is the WHAT and WHY of the domain rules; live symptom triage (dates off by one day,
|
||||
TIER_REQUIRED 403s) starts in mvp-debugging-playbook.
|
||||
---
|
||||
|
||||
# MotoVaultPro Vehicle Domain Reference
|
||||
|
||||
Authored 2026-07-07. All claims verified against repo code on that date. Where any doc contradicts this file or the code, the code wins.
|
||||
|
||||
## When to use / When NOT to use
|
||||
|
||||
Use this skill when you need the domain rules themselves: VIN structure and check-digit math, model-year resolution, fuel efficiency formulas and edge cases, DATE vs TIMESTAMP semantics, the tier model, or how VIN-decode results map onto the vehicle catalog.
|
||||
|
||||
Do NOT use this skill for:
|
||||
- OCR/Gemini pipeline mechanics (engines, WIF auth, timeouts, SDK sharp edges) -> `mvp-ocr-gemini-pipeline`
|
||||
- Debugging a live symptom on staging/prod -> `mvp-debugging-playbook`
|
||||
- Past incidents and settled battles in depth -> `mvp-failure-archaeology`
|
||||
- Architecture invariants and capsule layout -> `mvp-architecture-contract`
|
||||
- Adding a config knob or tier key -> `mvp-config-and-secrets`
|
||||
|
||||
## 1. VIN theory as implemented
|
||||
|
||||
A VIN (Vehicle Identification Number) is exactly 17 characters for model year 1981+. The letters I, O, and Q are never valid in a VIN (they look like 1 and 0). Both validators in this repo enforce the same pattern:
|
||||
|
||||
```
|
||||
^[A-HJ-NPR-Z0-9]{17}$
|
||||
```
|
||||
|
||||
- Python: `ocr/app/validators/vin_validator.py` (`MODERN_VIN_PATTERN`; a legacy 11-17 char pattern exists behind `allow_legacy=True` with a -0.2 confidence penalty)
|
||||
- Backend: `backend/src/features/vehicles/api/vehicles.controller.ts` (`VIN_REGEX`, rejects with 400 `INVALID_VIN` before calling the OCR service)
|
||||
|
||||
### Position semantics
|
||||
|
||||
| Positions | Name | Meaning |
|
||||
|---|---|---|
|
||||
| 1-3 | WMI | World Manufacturer Identifier (country + manufacturer) |
|
||||
| 4-8 | VDS | Vehicle Descriptor Section (model, body, engine) |
|
||||
| 9 | Check digit | Computed from all other positions (algorithm below) |
|
||||
| 10 | Model year code | 30-year cycle table (below) |
|
||||
| 11 | Plant | Assembly plant code |
|
||||
| 12-17 | Serial | Sequential production number |
|
||||
|
||||
### OCR confusion corrections
|
||||
|
||||
`VinValidator.TRANSLITERATION` in `ocr/app/validators/vin_validator.py` maps only characters that are INVALID in VINs to their likely intended values: `I -> 1`, `O -> 0`, `Q -> 0` (plus lowercase `i/o/q` and lowercase `l -> 1`). Deliberate constraint documented in the code: **B and S are valid VIN characters and must NOT be transliterated** (a naive "8 vs B, 5 vs S" correction would corrupt real VINs). Spaces and dashes are stripped first.
|
||||
|
||||
The extractor (`ocr/app/extractors/vin_extractor.py` + `extract_candidates()` in the validator) also handles OCR fragmentation (concatenating adjacent fragments) and spurious inserted characters (sliding 17-char windows plus one- and two-character deletion over 18-19 char strings), using the check digit to filter false candidates. This candidate machinery is a settled battle -- see `mvp-failure-archaeology` before "simplifying" it.
|
||||
|
||||
### Check-digit algorithm (position 9)
|
||||
|
||||
Implemented in `VinValidator.calculate_check_digit()`:
|
||||
|
||||
1. Transliterate each character to a value: digits map to themselves; letters map via `CHAR_VALUES`: A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8, J=1 K=2 L=3 M=4 N=5 P=7 R=9, S=2 T=3 U=4 V=5 W=6 X=7 Y=8 Z=9. (No I/O/Q; note P=7 with no 6, and R=9 with no 8 in that row -- this is per the standard, not a typo.)
|
||||
2. Multiply each position's value by its weight from `CHECK_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]` (position 9 itself has weight 0 and is skipped).
|
||||
3. Sum, take modulo 11. Remainder 0-9 is the check digit as a character; remainder 10 is the letter `X`.
|
||||
|
||||
A failed check digit does NOT reject the VIN in `validate()` -- it returns `is_valid=True` with `confidence_adjustment=-0.15` (some real VINs, especially non-North-American, do not comply). A passing check digit adds +0.1 confidence.
|
||||
|
||||
### Model-year code table (position 10)
|
||||
|
||||
`_VIN_YEAR_CODES` in `ocr/app/engines/gemini_engine.py` maps the position-10 character to a base year in the first cycle (1980-2009). The letters I, O, Q, U, Z and the digit 0 are not valid year codes.
|
||||
|
||||
| Code | Base | Code | Base | Code | Base |
|
||||
|---|---|---|---|---|---|
|
||||
| A | 1980 | L | 1990 | 1 | 2001 |
|
||||
| B | 1981 | M | 1991 | 2 | 2002 |
|
||||
| C | 1982 | N | 1992 | 3 | 2003 |
|
||||
| D | 1983 | P | 1993 | 4 | 2004 |
|
||||
| E | 1984 | R | 1994 | 5 | 2005 |
|
||||
| F | 1985 | S | 1995 | 6 | 2006 |
|
||||
| G | 1986 | T | 1996 | 7 | 2007 |
|
||||
| H | 1987 | V | 1997 | 8 | 2008 |
|
||||
| J | 1988 | W | 1998 | 9 | 2009 |
|
||||
| K | 1989 | X | 1999 | | |
|
||||
| | | Y | 2000 | | |
|
||||
|
||||
**The 30-year cycle ambiguity.** Codes repeat every 30 years (A = 1980 or 2010 or 2040). Disambiguation uses position 7, per NHTSA FMVSS No. 115 (MY2010+ vehicles must have an alphabetic position 7):
|
||||
|
||||
- Position 7 alphabetic -> add 30 (2010-2039 cycle)
|
||||
- Position 7 numeric -> 1980-2009 cycle, or 2040-2069 if that year is no more than 2 years in the future (`datetime.now().year + 2`)
|
||||
|
||||
This is implemented in `resolve_vin_year()` in `ocr/app/engines/gemini_engine.py`. **It was implemented backwards once** (numeric position 7 mapped to the 2010+ cycle) and fixed in commit `936753f`; the test suite `ocr/tests/test_resolve_vin_year.py` locks in the correct behavior. Do not re-derive this logic from memory -- read the function.
|
||||
|
||||
**The year is NEVER trusted from the LLM.** `GeminiEngine.decode_vin()` computes the year deterministically via `resolve_vin_year()`, passes it into the prompt as already-resolved, and overrides Gemini's returned year with the deterministic value (logging a warning on mismatch). Gemini is only trusted for make/model/trim/engine/transmission/body/drive/fuel, which require manufacturer knowledge.
|
||||
|
||||
### VIN decode path (for orientation only; details in mvp-ocr-gemini-pipeline)
|
||||
|
||||
`POST /api/vehicles/decode-vin` (pro-gated) -> `vehicles.controller.ts` regex validation -> `backend/src/features/ocr/external/ocr-client.ts:decodeVin()` -> OCR container `POST /decode/vin` (`ocr/app/routers/decode.py`) -> `GeminiEngine.decode_vin()` with Google Search grounding -> back through `vehiclesService.mapVinDecodeResponse()` (section 5). Frontend axios timeout for this call is 120s (`frontend/src/features/vehicles/api/vehicles.api.ts`) -- an older doc note saying 60s is stale. There is deliberately NO VIN decode cache (removed in `283ba6b` after race/staleness bugs).
|
||||
|
||||
## 2. Fuel math as implemented
|
||||
|
||||
Domain services: `backend/src/features/fuel-logs/domain/` -- `efficiency-calculation.service.ts`, `unit-conversion.service.ts`, `fuel-grade.service.ts`, orchestrated by `fuel-logs.service.ts`.
|
||||
|
||||
### Units model
|
||||
|
||||
`UnitSystem = 'imperial' | 'metric'` (defined in `backend/src/shared-minimal/utils/units.ts`, re-exported by `unit-conversion.service.ts`). The user's unit system comes from user settings (`fuel-logs/external/user-settings.service.ts`, default `imperial`).
|
||||
|
||||
Critical property: **stored values are unit-agnostic numbers in whatever system the user entered.** There is no conversion at write time; the unit system only selects labels and interpretation:
|
||||
|
||||
| unitSystem | fuelUnits means | distance means | efficiency label |
|
||||
|---|---|---|---|
|
||||
| imperial | gallons | miles | MPG |
|
||||
| metric | liters | kilometers | km/L |
|
||||
|
||||
Consequence: if a user switches unit systems, historical rows are NOT converted -- old numbers get reinterpreted under the new labels. This is the current implemented behavior, not an oversight to silently "fix" in passing; treat any change as a feature decision.
|
||||
|
||||
### Efficiency formula
|
||||
|
||||
`UnitConversionService.calculateEfficiency(distance, fuelUnits) = distance / fuelUnits` -- the same formula serves MPG (miles/gallon) and km/L (km/liter). The BACKEND domain `efficiencyLabel` for metric is km/L (an unused `MPG_TO_L100KM_FACTOR` and a `convertEfficiency` method exist -- `convertEfficiency` has no production callers as of 2026-07-07). The FRONTEND, however, diverges for metric users: `FuelLogForm.tsx:352` labels the calculated-efficiency field "L/100km" while the value it displays is computed as distance/units (i.e. km/L -- a live label/value mismatch), and `FuelLogsList.tsx:148` computes a local fallback as `(liters/km)*100` labeled "L/100km" (the settings screens also describe metric as L/100km). So a metric user sees "X km/L" when the API supplies efficiency but "Y L/100km" via the form and the list fallback. This backend/frontend divergence is a known inconsistency, NOT a settled convention -- do not flatly "correct" either side without treating it as a feature decision.
|
||||
|
||||
`EfficiencyCalculationService.calculateEfficiency(currentLog, previousOdometerReading, unitSystem)` picks the distance:
|
||||
|
||||
1. If `tripDistance > 0`: use it (`calculationMethod: 'trip_distance'`).
|
||||
2. Else if `odometerReading` present AND a previous odometer reading exists AND the delta is positive: use the delta (`calculationMethod: 'odometer'`).
|
||||
3. Else return `null` (no efficiency shown).
|
||||
|
||||
Edge cases that fall out of this, all intentional:
|
||||
- **First fill-up** with odometer method: no previous log -> `previousOdometerReading` is null -> efficiency is null. Efficiency is never stored; it is computed on read (the legacy `mpg` column was dropped in `migrations/003_drop_mpg_column.sql`).
|
||||
- Odometer rollback or duplicate reading (delta <= 0): efficiency null, not negative.
|
||||
- `fuelUnits <= 0`: null.
|
||||
- Vehicle stats (`getVehicleStats` in `fuel-logs.service.ts`) computes per-log efficiencies with `previousOdometerReading=null`, so odometer-method logs contribute 0 and are filtered out of `averageEfficiency` -- only trip-distance logs count toward the average. Know this before "fixing" a low average.
|
||||
|
||||
### Odometer vs tripDistance: XOR
|
||||
|
||||
Frontend zod schema (`frontend/src/features/fuel-logs/components/FuelLogForm.tsx`) enforces exactly-one via two `.refine()` calls: at least one of `odometerReading`/`tripDistance` must be > 0, AND both may not be set. The database enforces only the at-least-one half (`distance_required_check` in `migrations/002_enhance_fuel_logs_schema.sql`, recreated in `004`). `odometer` was made nullable and `trip_distance` widened to `DECIMAL(10,3)` in migration 004 to allow trip-only, fractional-distance logs.
|
||||
|
||||
### Dual legacy/enhanced schema -- which fields are canonical
|
||||
|
||||
One table (`fuel_logs`), two column generations, one repository (`data/fuel-logs.repository.ts`) with two APIs:
|
||||
|
||||
| Canonical (enhanced) column | Legacy column (dual-written) | API field |
|
||||
|---|---|---|
|
||||
| `date_time` TIMESTAMPTZ | `date` DATE | `dateTime` (ISO string) |
|
||||
| `fuel_units` DECIMAL(8,3) | `gallons` | `fuelUnits` |
|
||||
| `cost_per_unit` DECIMAL(6,3) | `price_per_gallon` | `costPerUnit` |
|
||||
| `trip_distance` DECIMAL(10,3) | -- | `tripDistance` |
|
||||
| `odometer` (nullable) | same column | `odometerReading` |
|
||||
| `fuel_type`, `fuel_grade`, `location_data` | -- | `fuelType`, `fuelGrade`, `locationData` |
|
||||
|
||||
`createEnhanced` dual-writes `gallons = fuelUnits` and `price_per_gallon = costPerUnit` for "legacy support". **The enhanced fields are canonical**; treat `gallons`/`price_per_gallon`/`date` as backfill/compatibility only. The enhanced response shape is `EnhancedFuelLogResponse` in `domain/fuel-logs.types.ts` (includes computed `efficiency?` and `efficiencyLabel`).
|
||||
|
||||
Convention break to know: `mapEnhancedRow` deliberately returns snake_case rows with numerics coerced (`parseFloat`), and `toEnhancedResponse` in the service does the camelCase mapping -- unlike the standard repository `mapRow()` pattern. This arrangement caused two incidents (#47 raw-row decision, #244 missing coercion); do not return raw pg rows and do not remove the coercion.
|
||||
|
||||
### Fuel type and grade
|
||||
|
||||
`fuel_type` is one of `gasoline | diesel | electric` (CHECK constraint). Valid grades per type, enforced both by `FuelGradeService` and a database trigger (`validate_fuel_grade()` in migration 002):
|
||||
|
||||
- gasoline: `87`, `88`, `89`, `91`, `93` (default 87)
|
||||
- diesel: `#1`, `#2`
|
||||
- electric: grade must be null
|
||||
|
||||
## 3. DATE vs TIMESTAMP semantics
|
||||
|
||||
**The rule.** A Postgres `DATE` column is a calendar date with no timezone (fuel_logs.date, maintenance_records.date, next_due_date, purchase_date, ...). It must flow as a plain `YYYY-MM-DD` string end-to-end: pg's DATE parser is overridden to return the raw string (`types.setTypeParser(1082, ...)` in `backend/src/core/config/database.ts` -- never remove this), APIs pass the string through, the frontend displays it with dayjs, and sorting is lexicographic (safe for ISO dates). Never write `new Date(dateString)`, `toISOString()`, or `toLocaleDateString()` against a DATE value -- each one applies a UTC or local-midnight conversion that shifts the date by a day for some timezone. A `TIMESTAMP WITH TIME ZONE` column (fuel_logs.date_time, created_at, updated_at) is a real instant: `new Date()` and ISO serialization are correct there (`toEnhancedResponse` does exactly this for `dateTime`).
|
||||
|
||||
**History (2026-03-23, three distinct traps fixed in one day, issue #237).** (a) pg returned DATE as a local-midnight Date object, which `toISOString()` shifted a day -- fixed by the type-parser override (`f0fc427`); (b) frontend `new Date("YYYY-MM-DD")` parses as UTC midnight, so `toLocaleDateString()` shifted it back -- fixed by dayjs display (`1e056f0`); (c) the OCR date parser used `toISOString().split('T')[0]` -- fixed with local-time formatting (`087f7b9`). This battle is settled; a "dates off by one day" symptom means someone reintroduced one of these three patterns.
|
||||
|
||||
## 4. Tier and subscription domain model
|
||||
|
||||
Source: `backend/src/core/config/feature-tiers.ts` (registry) + `backend/src/features/subscriptions/` (Stripe sync).
|
||||
|
||||
**Tier hierarchy** (`TIER_LEVELS`): `free(0) < pro(1) < enterprise(2)`. Higher tiers inherit lower-tier access via numeric comparison.
|
||||
|
||||
**Gated feature keys and vehicle limits**: the registry (`FEATURE_TIERS`) and `VEHICLE_LIMITS` (enforced via `canAddVehicle(tier, currentCount)`) live in `backend/src/core/config/feature-tiers.ts`; the current key catalog, the limit literals, and the gating mechanics are canonical in `mvp-config-and-secrets` section 3.
|
||||
|
||||
**Fail-open trap**: `canAccessFeature()` returns `true` for any feature key NOT in the registry -- a typo silently ungates the route -- and two parallel gating mechanisms exist with different failure behavior. Details in `mvp-config-and-secrets` section 3; read it before adding a gated route.
|
||||
|
||||
**Source of truth for a user's tier**: `user_profiles.subscription_tier` (Postgres enum `free|pro|enterprise`, default `free`; `backend/src/features/user-profile/migrations/002_add_subscription_and_deactivation.sql`). The auth plugin reads it into `request.userContext.subscriptionTier` on every authenticated request; all gating reads from `userContext`, never from Stripe directly. The subscriptions capsule keeps it synced from Stripe: `syncTierToUserProfile()` in `subscriptions/domain/subscriptions.service.ts` is called on subscription create/change/cancel and webhook events; `adminOverrideTier()` updates `subscriptions.tier` and `user_profiles.subscription_tier` atomically in one transaction; the grace-period job (`subscriptions/jobs/grace-period.job.ts`) downgrades lapsed users to `free`.
|
||||
|
||||
**The TIER_REQUIRED 403 contract** (what the frontend keys upgrade prompts on):
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "TIER_REQUIRED",
|
||||
"requiredTier": "pro",
|
||||
"currentTier": "free",
|
||||
"upgradePrompt": "Upgrade to Pro to ..."
|
||||
}
|
||||
```
|
||||
|
||||
Emitted by both `require-tier.ts` and `tier-guard.plugin.ts`. Keep this shape stable; do not invent per-route variants.
|
||||
|
||||
## 5. Vehicle catalog domain (platform capsule)
|
||||
|
||||
The platform capsule (`backend/src/features/platform/`) owns the year -> make -> model -> trim -> engine/transmission dropdown cascade used by vehicle forms. All routes are authenticate-only (not tier-gated).
|
||||
|
||||
- **Seeding**: `CatalogSeedService.seedIfEmpty()` (`platform/domain/catalog-seed.service.ts`) runs at backend startup (`backend/src/index.ts`), loading `engines.sql`, `transmissions.sql`, `vehicle_options.sql` from `features/platform/data/` (container path `/app/migrations/features/platform/data`) only if `vehicle_options` is empty. Seed failure is logged and startup continues (data can be imported later via admin UI) -- so an empty catalog is a possible runtime state, and VIN-decode matching degrades to all-`none` confidence when it happens.
|
||||
- **Caching**: dropdown results cache in Redis with 6-hour TTL per level (`platform/domain/platform-cache.service.ts`, `ttl = 6 * 3600`).
|
||||
- **VIN decode integration**: `vehiclesService.mapVinDecodeResponse()` (`backend/src/features/vehicles/domain/vehicles.service.ts`) takes Gemini's raw strings and matches each field against the catalog dropdown options, cascading: year (always `high` confidence if present, since it is deterministic) -> make -> model -> trim -> engine/transmission. Each downstream match only runs if every upstream field matched (`make.value` etc.). Per-field result is `{ value, sourceValue, confidence }` where confidence is `high` (exact case-insensitive match), `medium` (alphanumeric-normalized, prefix, contains, or longest reverse-contains match), or `none` (unmatched -- `value: null`, raw Gemini string preserved in `sourceValue` for display). `bodyType`, `driveType`, `fuelType` are display-only: never matched, always `confidence: 'none'`.
|
||||
|
||||
Practical implication: a correct Gemini decode can still return `value: null` fields if the catalog lacks that year/make/model row. That is a catalog-coverage problem, not a decode bug -- check `vehicle_options` before touching the OCR side.
|
||||
|
||||
## Provenance and maintenance
|
||||
|
||||
Authored 2026-07-07 by direct inspection of the files named above, plus `git log`/`git show` for incident commits (`936753f`, `1add6c8`, `56df5d4`, `283ba6b`, `f0fc427`, `1e056f0`, `087f7b9`, `574acf3`, `0d90829`). Volatile facts and how to re-verify each:
|
||||
|
||||
| Fact | Re-verify with |
|
||||
|---|---|
|
||||
| VIN regex / transliteration / weights / CHAR_VALUES | `sed -n '20,80p' ocr/app/validators/vin_validator.py` |
|
||||
| Year-code table and cycle logic | `sed -n '41,93p' ocr/app/engines/gemini_engine.py` |
|
||||
| Year never trusted from LLM | `grep -n "resolved_year" ocr/app/engines/gemini_engine.py` |
|
||||
| Frontend VIN decode timeout (120s, 2026-07-07) | `grep -n timeout frontend/src/features/vehicles/api/vehicles.api.ts` |
|
||||
| Efficiency formula and null cases | `cat backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts backend/src/features/fuel-logs/domain/unit-conversion.service.ts` |
|
||||
| XOR zod refinements | `grep -n -A3 refine frontend/src/features/fuel-logs/components/FuelLogForm.tsx` |
|
||||
| fuel_logs schema, constraints, grades | `cat backend/src/features/fuel-logs/migrations/*.sql` |
|
||||
| Dual-write legacy columns | `grep -n "legacy" backend/src/features/fuel-logs/data/fuel-logs.repository.ts` |
|
||||
| DATE type-parser override | `grep -n "setTypeParser" backend/src/core/config/database.ts` |
|
||||
| Tier levels (key/limit catalog: `mvp-config-and-secrets` section 3) | `grep -n "minTier\|VEHICLE_LIMITS" -A3 backend/src/core/config/feature-tiers.ts` |
|
||||
| TIER_REQUIRED 403 shape | `grep -rn -A4 "TIER_REQUIRED" backend/src/core/middleware/require-tier.ts backend/src/core/plugins/tier-guard.plugin.ts` |
|
||||
| Tier sync from Stripe | `grep -n "syncTierToUserProfile" backend/src/features/subscriptions/domain/subscriptions.service.ts` |
|
||||
| Catalog seed + 6h cache | `grep -n "seedIfEmpty\|6 \* 3600" backend/src/features/platform/domain/*.ts` |
|
||||
| matchField confidence ladder | `grep -n -A45 "private matchField" backend/src/features/vehicles/domain/vehicles.service.ts` |
|
||||
Reference in New Issue
Block a user