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

204 lines
17 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 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. |
**Branch state:** `feat/mechanic-mvp` at `a187a92`. Full stack: `be0553e` (A4) → `d2113a0` (B1) → `2739cc8` + `2fb978d` (B3) → `f0d7e83` (B4) → `a0bdcaa` + `1e5b822` (C1) → `7be8783` (C2) → `9109383` + `90b8c66` (C3) → `a187a92` (B3-vendor-align). All pushed to `origin` (Gitea).
**Test count on branch:** **85 mechanic tests pass** (38 models + 17 schemas + 9 deps + 21 endpoints across C1/C2/C3) in ~2s, no real DB needed.
### 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 D1 — inspections CRUD foundation (create + list)**
(From `docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md` Phase D.)
**Working dir:** `c:\NewProject\taxi-dashboard` (branch `feat/mechanic-mvp`, HEAD `90b8c66`)
⚠️ **Heads up** — Phase D introduces **write operations** + the **carry-over markers** logic, which is qualitatively different from Phase C's read-only endpoints:
- POST/PATCH endpoints need request-body validation (Pydantic schemas exist; verify they match the plan's body shapes)
- Carry-over: when a new inspection is created for a vehicle, copy unresolved markers from the previous inspection (`prev_inspection_id` self-reference). Needs careful design.
- Idempotency / race conditions: two mechanics opening the same vehicle simultaneously
- Real persistence with `_FakeSession`: tests need to verify the right `add()` / `flush()` / `refresh()` calls happened. The current `_FakeSession` only mocks read operations — needs extension for write paths (`add(obj)`, `flush()`, `refresh(obj)` to seed an `id`).
### 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 C полностью завершён (C1–C3, последний коммит 90b8c66, 83/83 тестов зелёных).
Следующая фаза — Phase D (inspections CRUD с carry-over markers) из плана
PremiumDriverApp/docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md.
⚠️ Перед спавном D1 implementer-subagent сделай brainstorming pass по трём решениям
из секции "Next task" в STATE.md: (1) carry-over inline-vs-async, (2) _FakeSession
write-path extension, (3) пересмотр Option C для real-DB conftest в Phase D.
Используй superpowers:brainstorming, потом superpowers:subagent-driven-development.
```
That prompt is the **single entry point** for the next session — Claude will read STATE.md, ground itself, brainstorm the three Phase D architecture decisions, then dispatch the D1 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): inspections CRUD with carry-over markers logic ← **NEXT**
- **Phase E** (5 tasks): MinIO storage layer + presigned upload/confirm + 302 read + celery thumbnail
- **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
≈ 28 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.