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

17 KiB
Raw Blame History

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 f0d7e83backend/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, TimestampMixinapp.models.base
  • get_dbapp.db (file: app/db/__init__.py)
  • get_current_userapp.auth.deps
  • Userapp.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.