13 KiB
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). |
Branch state: feat/mechanic-mvp at 90b8c66. Full stack: be0553e (A4) → d2113a0 (B1) → 2739cc8 + 2fb978d (B3) → f0d7e83 (B4) → a0bdcaa + 1e5b822 (C1) → 7be8783 (C2) → 9109383 + 90b8c66 (C3). All pushed to origin (Gitea).
Test count on branch: 83 mechanic tests pass (38 models + 15 schemas + 9 deps + 21 endpoints across C1/C2/C3) in ~2s, no real DB needed.
Live mechanic API surface (4 routes):
GET /api/v1/mechanic/healthz— livenessGET /api/v1/mechanic/me— current user + permissionsGET /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.baseget_db→app.db(file:app/db/__init__.py)get_current_user→app.auth.depsUser→app.models.user- Auth flow: OAuth2 password + Bearer JWT (not PIN as plan originally said)
- DB init:
Base.metadata.create_allat startup (not Alembic) + inlineALTER TABLE ADD COLUMN IF NOT EXISTSinapp/main.pylifespan - 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(modelMechanicInspection)mechanic_inspection_photos(modelMechanicInspectionPhoto)mechanic_damage_markers(modelMechanicDamageMarker)
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 C–F 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_idself-reference). Needs careful design. - Idempotency / race conditions: two mechanics opening the same vehicle simultaneously
- Real persistence with
_FakeSession: tests need to verify the rightadd()/flush()/refresh()calls happened. The current_FakeSessiononly mocks read operations — needs extension for write paths (add(obj),flush(),refresh(obj)to seed anid).
Recommendation for the next session: before dispatching the D1 implementer, decide:
- Whether the carry-over copy happens inside the create endpoint (simpler, atomic) or as a separate background step (e.g. Celery).
- How
_FakeSession.add/flush/refreshshould behave for write-path tests (auto-assign id? track inserted objects?). - Whether Phase D is the right place to introduce a tiny real-DB conftest after all (re-evaluate Option C from earlier).
These three decisions are NOT trivial and warrant a brainstorming pass at the start of the next session before any implementer is dispatched.
Read the D1 task definition in the plan before deciding. Also check what other relevant Pydantic schemas exist that we built in B3 (InspectionCreate, InspectionPatch, MarkerCreate, MarkerPatch).
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 depsPhase 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-inspectionsbucket — 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-mvpbranches in both repos - All new tables use
mechanic_prefix - Don't touch existing
app/api/inspections.pyorapp/models/inspection.py(vendor-imported) - All new mechanic source lives in flat files under
backend/app/api/withmechanic_*naming (mechanic.py,mechanic_schemas.py,mechanic_deps.py). Noapp/mechanic/subpackage — see "Plan-vs-reality overrides" table above. - Router prefix is
/v1/mechanic;/apiis added byinclude_router(..., prefix="/api")inmain.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_mechanicallows admins + members of any activeDepartmentwithdept_type='svc'. Use it as the default auth dep for all mechanic endpoints.