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

32 KiB
Raw Blame History

Premium Mechanic PWA — Implementation State

Last updated: 2026-05-17 (Phase 1 MVP code-complete, awaiting production deploy)

Status: READY TO DEPLOY (Phase J prep done, awaiting explicit user OK)

Backend (Phases B-F) and Frontend (Phases G-I) are feature-complete and pushed to Gitea. Deployment is the only step left — it requires SSH into root@100.64.0.12 (prod VDS), Docker compose restart, and Caddy reload. Not executed automatically. See "Deployment runbook" section below for the exact steps.

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.
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.
E E1: MinIO async presigned PUT URL wrapper DONE commit fe38981 — added presigned_put_url(bucket, key, *, content_type, expires_sec=300) to app/services/storage.py; PresignedUploadResponse.fields defaults to {}; 3 storage tests.
E E2: POST /inspections/{id}/photos/upload-url DONE commit 111661b — service create_pending_photo + _storage_key_for_photo helper in mechanic_service.py; endpoint in mechanic.py with 415-before-404 guard; UUID4 hex storage keys at mechanic/inspections/{id}/<uuid>.<ext>; 9 new tests (7 endpoint + 2 service).
E E3: POST /photos/{id}/confirm DONE commit 724b209 — service get_photo_by_id + confirm_photo (None-safe dim update, status→ready); endpoint verifies storage.exists, 409 if missing, 404 covers both photo-missing and inspection-mismatch; 9 new tests (7 endpoint + 2 service). _fake_photo_orm factory added to mechanic_fakes.py.
E E4: GET /photos/{id} + /thumb (302 redirect) DONE commit 8361ee1 — auth-guarded 302 to presigned MinIO GET URLs (1h expiry). Thumb endpoint falls back to storage_key when thumb_key is None so galleries degrade gracefully if E5 task hasn't run yet. 8 endpoint tests.
E E5: async thumbnail generation DONE commit b37a068generate_and_store_thumbnail worker (download → Pillow resize to 480px → upload → DB update) with try/except at each step (failures logged, return None, never crashes event loop). Triggered via asyncio.create_task from E3 confirm endpoint. _thumb_key_for predictable naming (<key-base>-thumb.jpg). pillow_heif.register_heif_opener() wrapped in try/except for dev-env tolerance. 6 new tests (5 storage unit + 1 endpoint trigger).
F F1+F2: markers CRUD (POST/PATCH/DELETE) DONE commit 7275a23 — 3 endpoints (POST /inspections/{id}/markers, PATCH /markers/{id}, DELETE /markers/{id}). Service helpers get_marker_by_id/create_marker/patch_marker/delete_marker. Stale comment in mechanic_damage_marker.py:54 fixed. _FakeSession.delete() added; _FakeSession.refresh() auto-sets created_at for server-default fields. 15 new tests. 159 mechanic tests total. Backend Phase 1 MVP feature-complete.
G G1-G5: Vite + React + TS + Tailwind + shadcn + PWA scaffold DONE commit e815955 in PremiumDriverApp/mechanic-pwa/frontend/ — 38 files, 8973 insertions. React 19 + Vite 8 + Tailwind v3 + TS strict + shadcn/ui (manually written to bypass broken @shadcn 4.7 "base-nova" style) + vite-plugin-pwa v1.3 (auto-bumped from v0.20 for Vite 8 compat). Service worker (sw.js + workbox, 10 precached entries). Cream palette (CSS vars from memory feedback_design_prefs). API client (ky) + auth store (zustand persist). 5 stub pages. Build: 91 KB gzip JS, 0 warnings.
H H1-H3: Login + Home + VehicleCard pages DONE commit 537548d — Login uses OAuth2 password flow (form-urlencoded) against /api/auth/login (NOT mechanic-prefixed). Home shows vehicle list with search + logout + /me fetch. VehicleCard has all 9 inspection-type buttons (top 4 primary, rest outline). Russian locale dates. Semantic Tailwind tokens throughout (no hex). Build: 110 KB gzip JS.
I I1-I3: photo capture/upload infra DONE commit fc4060dapi/photos.ts with presigned PUT pattern (not POST/FormData per plan), confirmPhoto(inspection_id, photo_id, meta) with both IDs in path per E3 backend. AuthImg component (fetches with Bearer JWT → blob URL → <img>) solving the <img>-can't-send-Authorization problem. CameraCapture uses <input type="file" capture="environment"> for max compat. PhotoSlot combines them.
I I4-I6: VehicleScheme + InspectionEditor + InspectionReview DONE commit a428395sedan-top.svg (top-view with 7 transparent zone rects), VehicleScheme.tsx with click+hover handlers + point-marker overlays + zone-count badges. inspections.ts typed with PhotoSummary/MarkerSummary. Editor wires scheme + 8 photo slots + marker list + finalize button (gated on status==="in_progress"); zone tap creates marker with damage_type="damaged"/severity="minor". Review is read-only with AuthImg thumb grid. Build: 113 KB gzip JS. Frontend Phase 1 MVP feature-complete.

Branch state: feat/mechanic-mvp at b37a068. Full stack: A4 → B1 → B3 + fix → B4 → C1 + fix → C2 → C3 + fix → B3-vendor-align → D1 + fix → D2 → D3 → test-refactor → E1 → E2 → E3 → E4 → E5. All pushed to origin (Gitea). See git log for full SHAs.

Test count on branch: 144 mechanic tests pass (38 models + 17 schemas + 9 deps + 9 storage + 71 endpoints) in ~2.5s, no real DB needed. Pillow image-resize tests use real PIL in-memory (verify JPEG magic header + dimensions); thumbnail task end-to-end uses mocked I/O.

Live mechanic API surface (11 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
  • POST /api/v1/mechanic/inspections/{inspection_id}/photos/upload-url — create pending photo row + presigned PUT URL (5min expiry)
  • POST /api/v1/mechanic/inspections/{inspection_id}/photos/{photo_id}/confirm — verify upload, mark ready, schedule thumbnail task
  • GET /api/v1/mechanic/photos/{photo_id} — auth-guarded 302 to presigned GET (1h)
  • GET /api/v1/mechanic/photos/{photo_id}/thumb — same with thumb_key, falls back to original

Phase E architecture decisions (RESOLVED 2026-05-17)

Decision Choice Rationale
Upload pattern PUT (single presigned URL) not POST/FormData Simpler PWA code: fetch(url, {method:'PUT', body:blob}). PresignedUploadResponse.fields defaults to {} for future POST migration.
Storage key naming mechanic/inspections/{insp_id}/<uuid4hex>.<ext> Human-readable path + unguessable suffix. UUID4 avoids enumeration.
Allowed content types image/{jpeg,png,heic,webp} Whitelist; 415 on others.
Confirm status semantics status="ready" set immediately on confirm; thumb generated async Frontend doesn't need to poll. Galleries fall back to original via E4 endpoint.
Thumbnail backend asyncio.create_task + Pillow (no Celery) Pillow + 15+ existing fire-and-forget call sites; no broker setup needed. Thumb resize is ~50ms for typical photo — acceptable on event loop.
Thumb size 480px max-edge, JPEG q=85 Matches services/download_wazzup_media.py precedent.
Auth model for read URLs Auth-required on /photos/{id} endpoint; presigned URLs themselves bearer-free (1h TTL) Same as existing media_proxy.py convention.

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

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.

Deployment runbook (Phase J — execute manually)

Status: prep done, awaiting user OK. Phase 1 MVP code is on feat/mechanic-mvp in both Gitea repos.

J0 — Pre-flight (verified)

  • Backend requirements.txt already has aiobotocore>=2.13.0, Pillow==10.4.0, pillow-heif==0.18.0.
  • Backend uses Base.metadata.create_all in lifespan (NOT Alembic) — new mechanic tables auto-create on container restart. No migration step needed.
  • MinIO bucket pp-inspections exists with CORS configured (Phase A3, ops/set-minio-cors.sh already applied).
  • Caddy fragment ops/Caddyfile.fragment ready in PremiumDriverApp repo.
  • Frontend dist/ build clean: 113 KB gzip JS, 12 KB CSS, sw.js + workbox precaching.
  • DNS: confirm mechanic.pptaxi.ru resolves to the VDS IP before Caddy reload.
  • Test mechanic user exists: need a User in the DB with is_admin=true OR membership in a Department with dept_type='svc'. Verify with: SELECT u.id, u.username, u.is_admin FROM users u LEFT JOIN user_departments ud ON ud.user_id=u.id LEFT JOIN departments d ON d.id=ud.department_id WHERE u.is_admin=true OR d.dept_type='svc';

J1 — Backend deploy (taxi-dashboard)

⚠️ Production touch — affects live CRM.

Plan said systemctl restart taxi-dashboard-backend.service + alembic upgrade headboth wrong for our setup. We use Docker blue/green + Base.metadata.create_all. Correct steps:

ssh root@100.64.0.12
cd /opt/sites/taxi-dashboard

# 1. Pull the mechanic branch into prod's tracking copy
git fetch origin feat/mechanic-mvp
git checkout feat/mechanic-mvp  # OR merge into main first if your release process needs that

# 2. Rebuild backend image (pulls in new requirements: Pillow, pillow-heif, aiobotocore should already be installed if it wasn't a fresh deploy, but rebuild to be safe)
docker compose build backend-blue backend-green

# 3. Rolling restart (blue first, then green — preserves availability)
docker compose up -d --no-deps backend-blue
sleep 10
docker logs taxi-backend-blue --tail 50  # check for clean startup + table-create logs
docker compose up -d --no-deps backend-green
sleep 10
docker logs taxi-backend-green --tail 50

# 4. Smoke test
curl -sS https://crm.pptaxi.ru/api/v1/mechanic/healthz
# Expected: {"status":"ok"}

Rollback if broken: git checkout main && docker compose up -d --no-deps backend-blue backend-green. The Base.metadata.create_all is additive (CREATE TABLE IF NOT EXISTS) — new tables stay, no data loss.

J2 — Frontend deploy (mechanic-pwa)

# 1. Build locally (already done; use the fresh dist/)
cd c:/NewProject/PremiumDriverApp/mechanic-pwa/frontend
npm run build

# 2. Create target dir on VDS if first deploy
ssh root@100.64.0.12 'mkdir -p /opt/sites/mechanic-pwa/dist'

# 3. Sync the built assets
rsync -avz --delete dist/ root@100.64.0.12:/opt/sites/mechanic-pwa/dist/

# 4. Install Caddy fragment (first deploy only)
ssh root@100.64.0.12
cd /opt/sites/mechanic-pwa
# (assumes PremiumDriverApp repo is checked out at /opt/sites/mechanic-pwa/repo;
#  if not, scp ops/Caddyfile.fragment instead)
cp /opt/sites/mechanic-pwa/repo/ops/Caddyfile.fragment /etc/caddy/sites-enabled/mechanic.pptaxi.ru
caddy validate --config /etc/caddy/Caddyfile  # OR appropriate validation cmd
systemctl reload caddy

# 5. Smoke test
curl -I https://mechanic.pptaxi.ru
# Expected: 200 OK, Content-Type: text/html
curl -sS https://mechanic.pptaxi.ru/manifest.webmanifest | head -20
# Expected: JSON with "name": "Premium Механик"

Rollback: rm /etc/caddy/sites-enabled/mechanic.pptaxi.ru && systemctl reload caddy removes the route; old dist/ can be restored from previous rsync run if you take a snapshot first.

J3 — Smoke test (manual)

Open https://mechanic.pptaxi.ru on a mobile browser (Android Chrome / iOS Safari):

  1. Login with the test mechanic account
  2. Search for any car → open card → start "Передача"
  3. Tap 2 zones on the scheme → markers appear with red badges
  4. Snap 1 photo (any slot) → wait for "загружено" toast → thumb appears
  5. Tap "Завершить осмотр" → redirected to review page showing photo + markers
  6. Verify in DB:
    SELECT id, vehicle_id, type, status, performed_by, started_at, finished_at
    FROM mechanic_inspections ORDER BY id DESC LIMIT 3;
    SELECT id, side, slot_index, status, thumb_key IS NOT NULL AS has_thumb
    FROM mechanic_inspection_photos WHERE inspection_id=<NEW_ID>;
    SELECT id, side, damage_type, carried_over_from_id, resolved
    FROM mechanic_damage_markers WHERE inspection_id=<NEW_ID>;
    
  7. Verify in MinIO console (https://s3-admin.pptaxi.ru) bucket pp-inspections: see the mechanic/inspections/<NEW_ID>/<uuid>.jpg original + <uuid>-thumb.jpg next to it.
  8. PWA install: tap browser menu → "Add to Home Screen" → opens fullscreen.

If all 8 pass → MVP is live. File UX issues as Gitea issues.

Acceptance criteria (from plan §3)

  • Mechanic can log in on mechanic.pptaxi.ru from mobile
  • Find vehicle by license plate
  • Open vehicle card, see recent inspections
  • Start handover / return / periodic / ad-hoc inspection
  • On return: carry-over markers from prev appear with carried_over_from_id
  • Take ≥4 photos in slots, uploaded directly to MinIO via presigned PUT
  • Click zones on SVG scheme → point markers added
  • Finalize inspection (status=completed, finished_at auto-set)
  • View read-only review with all photos + markers
  • PWA installable to home screen on Android + iOS

Next task (post-deploy)

Task F1 — Markers CRUD (Phase F starts; only 2 tasks) — DONE see table above

After successful J3 smoke test, Phase 1 MVP is complete. Phase 2 backlog: polygon annotation editor (Konva), offline IndexedDB queue, native APK wrap, push notifications, VendorBridge data import, admin stats dashboard, QR scanner.

(From docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md Phase F.)

Working dir: c:\NewProject\taxi-dashboard (branch feat/mechanic-mvp, HEAD b37a068)

Phase F adds the marker endpoints — finally exposes the carry-over data we've been carefully copying since D1. After F, backend Phase 1 MVP is feature-complete and Phase G begins frontend work.

Two endpoints:

  • POST /inspections/{inspection_id}/markers — create a new damage marker (request: MarkerCreate from B3)
  • PATCH /markers/{marker_id} — update marker (request: MarkerPatch from B3)

The plan likely also wants DELETE /markers/{marker_id} — verify in the plan. There may also be a "resolve marker" endpoint that's just a PATCH with resolved=True.

Important context:

  • MechanicDamageMarker has a stale comment at line 54 (refs old DamageType values like rust/glass/paint) — fix in F1 while touching this model area.
  • MarkerCreate and MarkerOut schemas already exist in mechanic_schemas.py from B3.
  • The carry-over markers (carried_over_from_id IS NOT NULL) deserve a thought: PATCH should be allowed to update them (mechanic confirms the existing damage is still there with current photo); DELETE should probably be NOT allowed on carry-overs (only on freshly-created markers) to preserve audit trail. Verify this in the plan.

Phase F implementer pattern: apply the same Phase D path overrides (flat app/api/, MechanicDamageMarker not DamageMarker, etc.). Reuse _FakeSession + _fake_marker_orm from mechanic_fakes.py.

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 1 MVP code-complete.
Backend (14 routes, 159 тестов) + Frontend (113 KB gzip PWA) запушены на feat/mechanic-mvp.

Deployment runbook готов в STATE.md секция "Deployment runbook". Выполни J1+J2+J3:
1. SSH в root@100.64.0.12, git pull feat/mechanic-mvp, docker compose rebuild + rolling restart blue→green
2. rsync mechanic-pwa/frontend/dist/ на VDS + caddy reload mechanic.pptaxi.ru
3. Smoke test на мобильном браузере по чек-листу из runbook'а

После успешного deploy: merge feat/mechanic-mvp → main в обоих репах (PremiumDriverApp + taxi-dashboard).

That prompt is the single entry point for the next session — Claude reads STATE.md, executes the deployment runbook, runs smoke tests, merges to main.

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
  • Phase E (5 tasks) done
  • Phase F (2 tasks) done — backend Phase 1 MVP feature-complete (14 routes, 159 tests)
  • Phase G (5 tasks) done — frontend scaffold (91 KB gzip after G; 113 KB after I)
  • Phase H (3 tasks) done — Login + Home + VehicleCard
  • Phase I (6 tasks) done — CameraCapture + AuthImg + PhotoSlot + VehicleScheme + Editor + Review
  • Phase J (4 tasks): code-complete, deployment runbook ready, awaiting user OKHERE

Code work is done. Phase 1 MVP delivery is one ssh root@100.64.0.12 + Docker compose restart + rsync away.

≈ 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.