Files
mechanic-pwa/docs/superpowers/STATE.md
T

227 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Premium Mechanic PWA — Implementation State
> Last updated: 2026-05-17 (end of session 2 — Phase B + Phase C + Phase D done)
## What is done
| Phase | Task | Status | Artifact |
|---|---|---|---|
| 0 | Git infrastructure (Gitea + repos + feature branches) | ✅ DONE | repos exist, ssh keys configured |
| A | A1: Recon TaxiDashboard backend structure | ✅ DONE | `docs/backend-layout-notes.md` |
| A | A2: Caddy fragment for `mechanic.pptaxi.ru` | ✅ DONE | `ops/Caddyfile.fragment` |
| A | A3: MinIO CORS on `pp-inspections` bucket | ✅ DONE | `ops/set-minio-cors.sh`, applied, preflight 204 |
| A | A4: SQLAlchemy models (`MechanicInspection`, `MechanicInspectionPhoto`, `MechanicDamageMarker`) | ✅ DONE | commit `be0553e` on `feat/mechanic-mvp` |
| B | B1: Mechanic module skeleton + `/healthz` | ✅ DONE | commit `d2113a0` — flat `backend/app/api/mechanic.py` with `APIRouter(prefix="/v1/mechanic")`, registered in `app/main.py` with `prefix="/api"` → final URL `/api/v1/mechanic/healthz` |
| B | B2: SQLAlchemy models registered + tests | ✅ DONE | **already shipped in A4** (`be0553e`) — 38 metadata tests in `backend/tests/test_mechanic_models.py`, no extra B2 commit |
| B | B3: Pydantic schemas | ✅ DONE | commit `2739cc8` + fix `2fb978d` — 14 schemas + 6 Literals in `backend/app/api/mechanic_schemas.py`, 15 behavioral tests in `backend/tests/test_mechanic_schemas.py`; fix added `= None` defaults to `Optional[X]` fields and tightened `MarkerOut.damage_type`/`severity` to `DamageType`/`Severity` Literals |
| B | B4: Auth + DB session dependencies | ✅ DONE | commit `f0d7e83``backend/app/api/mechanic_deps.py` re-exports `get_db`+`get_current_user`, adds `require_mechanic` guard (admin OR member of any active `dept_type='svc'` department); 9 tests in `backend/tests/test_mechanic_deps.py` use fake-DB stubs (no real session needed)
| C | C1: `GET /me` endpoint | ✅ DONE | commit `a0bdcaa` + signature fix `1e5b822` — appended to `backend/app/api/mechanic.py`; returns `MeOut` from `require_mechanic` user; `permissions` is `["admin", "mechanic:inspect"]` for admins else `["mechanic:inspect"]`. Test fixtures (`client`, `override_mechanic`, `_fake_user`) in `backend/tests/test_mechanic_endpoints.py` use FastAPI `TestClient` + `app.dependency_overrides` per Option A — no real DB / no async. 6 endpoint tests. |
| C | C2: `GET /vehicles` list with `q`+`limit` search | ✅ DONE | commit `7be8783` — new `backend/app/api/mechanic_service.py` with `search_vehicles_with_last_inspection` (single aggregated subquery + outerjoin to avoid N+1 for last completed inspection per vehicle); endpoint in `mechanic.py`; new `VehicleListResponse` schema; 8 tests (6 endpoint + 2 service-stmt) with `_FakeSession.execute()` returning canned rows. Search hits plate (`CarV2.number`) + VIN only. |
| C | C3: `GET /vehicles/{id}` detail + bound /vehicles limit | ✅ DONE | commit `9109383` + style fix `90b8c66` — new `VehicleDetail(VehicleSummary)` + `InspectionSummary(_Base)` schemas; service `get_vehicle_by_id` + `recent_mechanic_inspections`; endpoint raises 404 when missing, `last_inspection_at` derived from first completed inspection in the list; applied `Query(20, ge=1, le=200)` bounds to C2's `/vehicles` limit param. 7 new tests (5 detail + 2 bounds). Service is now FastAPI-free (404 raise lives in endpoint, matching `automations.py` convention). |
| D-pre | B3 schemas widened from vendor recon (`a187a92`) | ✅ DONE | After read-only recon of `car_inspections` (14k rows, 2yr): `InspectionType` extended 4→9 (added `initial`, `seizure`, `equipment-change`, `pre-repair`, `post-repair`); `DamageType` fully replaced 7→16 (removed `rust`/`glass`/`paint` which had 0 vendor occurrences; added real-world types from vendor distribution: `damaged`, `chip`, `scuff`, `burn`, `bent`, `puncture`, `bulge`, `not-working`, etc.). `Severity` unchanged (4 values, optional — vendor populates only 3%). 2 new positive-coverage tests. Stale comment in `mechanic_damage_marker.py:54` references old DamageType values — fix in Phase F when touching that model. |
| D | D1: `POST /inspections` with inline carry-over markers | ✅ DONE | commits `39a6579` + correctness fix `ee3d370` — service `start_inspection` + `latest_completed_inspection` (with `NULLS LAST` ordering); endpoint returns 201 with full InspectionDetail via re-query with `selectinload(photos, markers)`. Carry-over: copies all **unresolved** markers from prev completed inspection (`resolved=False` filter — closed defects don't reopen), each new marker has `carried_over_from_id=source.id`, `photo_id=None`, `resolved=False`. `_FakeSession` extended with `add`/`flush`/`commit`/`refresh` write-path methods + `rows_per_call` queue for multi-SELECT service flows. 9 new tests (6 endpoint + 3 service). |
| D | D2: `GET /inspections/{id}` with photos+markers | ✅ DONE | commit `1776a81` — service `get_inspection_by_id_with_relations` (selectinload both relationships, `scalar_one_or_none`); endpoint raises 404 with `"inspection not found"` parallel to vehicle's 404. D1's inline re-query block refactored to call the same helper (DRY). 6 new tests (4 endpoint + 2 service introspecting `stmt._with_options`). |
| D | D3: `PATCH /inspections/{id}` status/notes/finished_at | ✅ DONE | commit `aefe117` — service `get_inspection_by_id` (thin `session.get` wrapper) + `patch_inspection` (auto-sets `finished_at=now(UTC)` when status→completed AND finished_at not in payload; `setattr` loop applies all `model_dump(exclude_unset=True)` fields). Endpoint re-loads via `get_inspection_by_id_with_relations` for the response. 8 new tests (6 endpoint + 2 service). Endpoint and service both named `patch_inspection` — disambiguated by module path in code, by `as svc_patch` alias in tests. |
| D | Housekeeping: extract test fakes to `mechanic_fakes.py` | ✅ DONE | commit `eedbf23` — pure refactor. Moved `_FakeResult`, `_FakeSession`, `_fake_user`, `_fake_car`, `_fake_inspection`, `_fake_inspection_orm`, `_fake_marker_orm` from `test_mechanic_endpoints.py` (was 899 lines) to new `backend/tests/mechanic_fakes.py` (207 lines). Test file dropped to 697 lines. Pytest fixtures (`client`, `override_mechanic`, `override_db`) stayed in the test file. Phase E tests will import from `mechanic_fakes` directly. |
**Branch state:** `feat/mechanic-mvp` at `eedbf23`. Full stack: `be0553e` (A4) → `d2113a0` (B1) → `2739cc8` + `2fb978d` (B3) → `f0d7e83` (B4) → `a0bdcaa` + `1e5b822` (C1) → `7be8783` (C2) → `9109383` + `90b8c66` (C3) → `a187a92` (B3-vendor-align) → `39a6579` + `ee3d370` (D1) → `1776a81` (D2) → `aefe117` (D3) → `eedbf23` (test-fakes-refactor). All pushed to `origin` (Gitea).
**Test count on branch:** **108 mechanic tests pass** (38 models + 17 schemas + 9 deps + 44 endpoints across C1-C3 + D1-D3) in ~2s, no real DB needed.
**Live mechanic API surface (7 routes):**
- `GET /api/v1/mechanic/healthz`
- `GET /api/v1/mechanic/me`
- `GET /api/v1/mechanic/vehicles?q=&limit=`
- `GET /api/v1/mechanic/vehicles/{vehicle_id}`
- `POST /api/v1/mechanic/inspections` — create with carry-over for type=return
- `GET /api/v1/mechanic/inspections/{inspection_id}` — full detail with photos+markers
- `PATCH /api/v1/mechanic/inspections/{inspection_id}` — partial update + auto finished_at
### Vendor data reality (from recon 2026-05-17 — 14,013 inspections, 2 years)
| Metric | Value | Implication |
|---|---|---|
| Inspections with damage | 1,941 / 14,013 = 13.9% | Most inspections have empty marker list — UI optimize for that |
| Median markers per inspection | 2 | Inline carry-over copy is cheap |
| p95 markers | 10 | Confirms no async needed |
| Max markers | 34 | Worst case still ~50 inserts on commit |
| Median photos per inspection | 11 (`all_photo_ids`) | Includes 9 sides + damage photos |
| Max photos per inspection | 30 | Set MinIO presigned batch size accordingly |
| Carry-over in vendor | **None — denormalized snapshot** | Our `prev_inspection_id`+`carried_over_from_id` FK approach is net improvement |
| Vendor inspection status field | **None — write-once when completed** | Our `in_progress`/`completed`/`cancelled` is greenfield, no conflicts |
**Live mechanic API surface (4 routes):**
- `GET /api/v1/mechanic/healthz` — liveness
- `GET /api/v1/mechanic/me` — current user + permissions
- `GET /api/v1/mechanic/vehicles?q=&limit=` — list with search (1≤limit≤200)
- `GET /api/v1/mechanic/vehicles/{vehicle_id}` — detail with recent inspections
## Where things live
### Repos (Gitea: `http://100.64.0.9:3001/tremble7681`)
| Repo | Local clone | Working branch |
|---|---|---|
| `mechanic-pwa` | `c:\NewProject\PremiumDriverApp\` | `feat/mechanic-mvp` |
| `taxi-dashboard` | `c:\NewProject\taxi-dashboard\` | `feat/mechanic-mvp` |
`taxi-dashboard` clone has two remotes: `origin` → Gitea, `vds` → SSH to `/opt/sites/taxi-dashboard/` on 100.64.0.12 (for pulling back-and-forth with prod).
### Reference paths (real, verified — see `docs/backend-layout-notes.md`)
- `Base`, `TimestampMixin``app.models.base`
- `get_db``app.db` (file: `app/db/__init__.py`)
- `get_current_user``app.auth.deps`
- `User``app.models.user`
- Auth flow: **OAuth2 password + Bearer JWT** (not PIN as plan originally said)
- DB init: **`Base.metadata.create_all`** at startup (not Alembic) + inline `ALTER TABLE ADD COLUMN IF NOT EXISTS` in `app/main.py` lifespan
- Backend Docker container: `taxi-backend-green` / `taxi-backend-blue` (blue/green)
- MinIO container: `taxi-minio`
- Models registered in: `app/models/__init__.py` (imports + `__all__`)
### Naming clash decision
Existing `app/api/inspections.py` + model `CarInspection` (table `car_inspections`) hold **vendor-imported read-only** data from NaughtySoft. Our new tables use the **`mechanic_`** prefix to coexist:
- `mechanic_inspections` (model `MechanicInspection`)
- `mechanic_inspection_photos` (model `MechanicInspectionPhoto`)
- `mechanic_damage_markers` (model `MechanicDamageMarker`)
### Plan-vs-reality overrides applied in Phase B (Authoritative)
Three places where the original plan (`docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md`) was overridden during execution because Phase A1 recon showed the codebase doesn't match the plan's assumptions:
| Plan said | Actual implementation | Why |
|---|---|---|
| `backend/app/mechanic/router.py` (subpackage) | `backend/app/api/mechanic.py` (flat) | TaxiDashboard uses flat `app/api/*.py` files; no subpackages |
| `backend/app/mechanic/schemas.py` (subpackage) | `backend/app/api/mechanic_schemas.py` (flat sibling) | Same convention |
| `backend/app/mechanic/deps.py` (subpackage) | `backend/app/api/mechanic_deps.py` (flat sibling) | Same convention |
| Router prefix `/api/v1/mechanic` | Router prefix `/v1/mechanic`, with `app.include_router(..., prefix="/api")` in `main.py` | Matches existing convention (`inspections.py``/v2/inspections` + include with `/api`). Final URL is identical: `/api/v1/mechanic/...` |
| `require_mechanic` checks `user.dept.lower() in (...)` | Queries `UserDepartment ⨝ Department WHERE dept_type='svc' AND is_active`, admin shortcuts before DB hit | `User` has no `dept` attribute; departments are M2M via `user_departments`. `DEPT_TYPES=['ops','svc','adm']`, `svc` = service-type (мастерская) |
When dispatching Phase CF implementers, copy this override map into their context so they don't follow the stale subpackage / dept paths from the plan.
## Phase C testing-infrastructure decision (RESOLVED — Option A in force)
Phase C runs against `FastAPI TestClient` (sync) + `app.dependency_overrides[require_mechanic]` and `app.dependency_overrides[get_db]` to inject fake user / fake session. Fake session is a `SimpleNamespace`-flavored `_FakeSession` class in `tests/test_mechanic_endpoints.py` that supports `.execute(stmt)` (returns `_FakeResult` seeded with rows) and `.get(model, pk)` (returns seeded object or `None`).
No `conftest.py`, no `pytest-asyncio`, no real DB. SQL correctness is deliberately deferred to Phase J e2e.
If Phase D/E/F needs a more complex query shape (e.g. `.scalars().one()` / `.first()`), extend `_FakeResult.scalars()` accordingly — currently it returns `self` and supports only `.all()`. See `test_mechanic_endpoints.py:_FakeResult` docstring.
## Next task
### **Task E1 — MinIO async client wrapper** (Phase E starts)
(From `docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md` Phase E, 5 tasks total: E1 MinIO wrapper, E2 presigned upload generation, E3 photo confirm endpoint, E4 photo-read 302 redirect, E5 celery thumbnail.)
**Working dir:** `c:\NewProject\taxi-dashboard` (branch `feat/mechanic-mvp`, HEAD `eedbf23`)
⚠️ **Heads up — Phase E introduces a new external dependency layer (MinIO/S3) and possibly Celery.** Qualitatively new domain:
- **S3 client (`aioboto3` or `boto3` async wrapper)** — needs a thin wrapper module so endpoints don't import the SDK directly. Check what's already in `requirements.txt` (production has `media_proxy.py` and the existing `taxi-minio` container — there may be a usable abstraction already).
- **Presigned URL generation** for client-side direct uploads — credentials, expiration, content-type contract.
- **Celery for thumbnail generation** — this is a **major new dependency**. The TaxiDashboard backend may already have Celery (check `app/services/` or `app/tasks/`). If not, adding it is a big infrastructure step that warrants brainstorming + maybe a separate prep task.
- **Photo confirm flow** — client uploads to presigned URL → calls our `POST /photos/{id}/confirm` → backend updates `status='ready'` (matching the `MechanicInspectionPhoto.status` enum from B3).
- **302 redirect for reads** — `GET /photos/{id}` issues a 302 with a presigned download URL (avoids streaming binary through FastAPI).
**Recommendation for the next session:** before dispatching E1 implementer, **brainstorm**:
1. **Existing MinIO integration audit** — check `app/api/media_proxy.py`, look for existing S3 client setup, env vars (`MINIO_*` likely already in `.env`), bucket naming (the `pp-inspections` bucket is already set up per Phase A3).
2. **Celery audit** — is Celery already running? If yes, where; if no, do we want it for Phase 1 or can thumbnails be sync-on-confirm (faster ship, slightly slower confirm endpoint).
3. **Auth model for presigned URLs** — they're time-bounded but anyone with the URL can hit MinIO directly. Are we OK with that for inspection photos? Or do we want signed URLs to also encode a JWT claim?
Read the E1 task definition in the plan before deciding. The B3 `PresignedUploadRequest` / `PresignedUploadResponse` schemas are already shipped and define the API contract — the endpoints just need to be wired up.
### Phase D architecture decisions (RESOLVED 2026-05-17)
1. **Carry-over: inline в POST /inspections** (atomic in the same transaction). Until carry-over copy completes, the new inspection isn't "ready" for the UI — async would create race conditions and complicate testing without a meaningful payoff for ~10-20 markers per copy.
2. **`_FakeSession` write-path extension.** Add the following to the existing `_FakeSession` in `tests/test_mechanic_endpoints.py`:
- `add(obj)` — appends to `self.added` list; auto-assigns `obj.id` if not set, popping from `_next_ids` queue if provided, else incrementing `_next_id_counter` (starts at 1000 to avoid collision with test-seeded ids in the 1-99 range)
- `async flush()` — no-op + `self.flush_count += 1` (ids already assigned in `add`)
- `async commit()` — no-op + `self.commit_count += 1`
- `async refresh(obj)` — no-op + `self.refresh_count += 1`
- **`rows_per_call: Optional[list[Sequence]]`** kwarg — if provided, each `execute()` call pops one item from this queue and returns it as the result rows. This handles multi-query service flows (e.g. `start_inspection` does `select(prev_inspection)` then `select(prev_markers)`).
- Existing `execute(stmt)` and `get(model, pk)` keep their behavior.
- All extensions backward-compatible with current 83 tests.
3. **Test infrastructure: stay on Option A, do NOT add real DB.** SQL specifics (selectinload, cascade, JSONB) will be validated in Phase J e2e. aiosqlite ≠ Postgres (JSONB mismatch), testcontainers is too heavy for 3 endpoints, two-paradigm test suite is more pain than gain. Extend the mock instead (see point 2).
### Phase D path overrides (apply to D1, D2, D3)
Same flat-file pattern as Phase B-C; map plan's stale paths:
| Plan says | Use instead |
|---|---|
| `backend/app/mechanic/router.py` | `backend/app/api/mechanic.py` (append) |
| `backend/app/mechanic/service.py` | `backend/app/api/mechanic_service.py` (append) |
| `backend/app/mechanic/schemas.py` | `backend/app/api/mechanic_schemas.py` (already has all needed shapes from B3) |
| `backend/tests/test_mechanic/test_inspections.py` | `backend/tests/test_mechanic_endpoints.py` (append) |
| `Inspection` model | `MechanicInspection` (`app/models/mechanic_inspection`) |
| `DamageMarker` model | `MechanicDamageMarker` (`app/models/mechanic_damage_marker`) |
| `InspectionPhoto` model | `MechanicInspectionPhoto` (`app/models/mechanic_inspection_photo`) |
| `service.get_vehicle_or_404` (plan) | `service.get_vehicle_by_id` + endpoint-level `None`-check + `HTTPException(404)` raise (C3 fix) |
| `from fastapi import HTTPException` inline in service | At endpoint level only; service stays FastAPI-free |
| 404 raise inside endpoint via inline `from fastapi import HTTPException` | Use top-of-file import (already there from C3) |
Pydantic schemas (already shipped in B3):
- `InspectionCreate` (vehicle_id, type, driver_id) — request body for POST
- `InspectionPatch` (status, notes, finished_at) — request body for PATCH
- `InspectionDetail` (full response with photos + markers list) — response model
Phase B3 schemas use `model_config = ConfigDict(from_attributes=True)` via `_Base`, so SQLAlchemy ORM instances serialize automatically with `response_model=InspectionDetail`.
## How to resume in a new session
In a new Claude Code chat in `c:\NewProject\`, paste this prompt:
```
Продолжаем проект Premium Mechanic PWA.
Прочитай PremiumDriverApp/docs/superpowers/STATE.md — там полное состояние и следующая задача.
Phase D полностью завершён (D1–D3, последний коммит eedbf23, 108/108 тестов зелёных).
Backend read+write API готов: 7 routes. Следующая фаза — Phase E (MinIO storage layer:
presigned upload + confirm + 302 read + celery thumbnail) из плана
PremiumDriverApp/docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md.
⚠️ Перед спавном E1 implementer сделай 3 проверки:
1. Audit existing MinIO integration: app/api/media_proxy.py, env vars, S3 client.
2. Audit Celery: уже установлен? Где? Иначе — sync thumbnails в Phase 1.
3. Решить про auth для presigned URLs (см. секцию "Next task" в STATE.md).
Используй superpowers:brainstorming для решений, потом superpowers:subagent-driven-development.
```
That prompt is the **single entry point** for the next session — Claude will read STATE.md, audit existing MinIO/Celery state, resolve the auth-for-URLs decision, then dispatch the E1 implementer.
## Remaining roadmap (Phase 1 MVP)
- ~~**Phase B** (4 tasks)~~ ✅ done — module skeleton, models, schemas, auth deps
- ~~**Phase C** (3 tasks)~~ ✅ done — GET /me, GET /vehicles, GET /vehicles/{id}
- ~~**Phase D** (3 tasks)~~ ✅ done — POST /inspections (+ carry-over), GET /inspections/{id}, PATCH /inspections/{id}
- **Phase E** (5 tasks): MinIO storage layer + presigned upload/confirm + 302 read + celery thumbnail ← **NEXT**
- **Phase F** (2 tasks): markers create/update/delete
- **Phase G** (5 tasks): Vite + React + Tailwind + shadcn + PWA scaffold
- **Phase H** (3 tasks): Login + Home + VehicleCard pages
- **Phase I** (6 tasks): CameraCapture + PhotoSlot + VehicleScheme + InspectionEditor + InspectionReview
- **Phase J** (4 tasks): e2e test + deploy backend + deploy frontend + smoke test
≈ 25 tasks left until MVP live on `mechanic.pptaxi.ru`.
## Known external dependencies
- MinIO `pp-inspections` bucket — CORS already configured, presigned uploads will work
- Caddy on 100.64.0.12 — fragment ready, will be wired during Phase J deploy
- Docker blue/green deployment on VDS — backend re-deployment will need a touch of `docker compose` (Phase J)
- Frontend deployment target: `/opt/sites/mechanic-pwa/dist/` on VDS (Phase J)
## Conventions reminder
- All new code in `feat/mechanic-mvp` branches in both repos
- All new tables use `mechanic_` prefix
- Don't touch existing `app/api/inspections.py` or `app/models/inspection.py` (vendor-imported)
- All new mechanic source lives in **flat files under `backend/app/api/`** with `mechanic_*` naming (`mechanic.py`, `mechanic_schemas.py`, `mechanic_deps.py`). No `app/mechanic/` subpackage — see "Plan-vs-reality overrides" table above.
- Router prefix is `/v1/mechanic`; `/api` is added by `include_router(..., prefix="/api")` in `main.py`. Final URLs always `/api/v1/mechanic/*`.
- Phase C+ endpoint tests: pending the test-infrastructure decision (Option A / B / C above). Do NOT introduce `pytest-asyncio`, `conftest.py`, or a real DB without first resolving that decision.
- `require_mechanic` allows admins + members of any active `Department` with `dept_type='svc'`. Use it as the default auth dep for all mechanic endpoints.