# Premium Mechanic PWA — Implementation State > Last updated: 2026-05-17 (Phase 1 MVP LIVE IN PRODUCTION) ## Status: 🟢 **DEPLOYED — Phase 1 MVP LIVE on https://mechanic.pptaxi.ru** All 4 deployment phases executed successfully: - ✅ **J1 Backend**: `git push vds feat/mechanic-mvp` → checkout on VDS → `docker build` → rolling restart blue+green → healthz 5/5 green on `https://crm.pptaxi.ru/api/v1/mechanic/healthz` - ✅ **J2 Frontend**: `scp dist/` → VDS at `/opt/sites/mechanic-pwa/dist/` (448K) → added Caddy block + bind mount → `docker compose up -d --force-recreate caddy` → Let's Encrypt cert auto-issued → HTTPS 200 on `https://mechanic.pptaxi.ru/` - ✅ **J3a Merge**: `feat/mechanic-mvp` → `main` (ff-only) in BOTH repos, pushed to Gitea + VDS - ✅ **J3b Prod sync**: VDS now on `main` branch, healthz still green **Smoke checks passed:** | Check | Result | |---|---| | `GET https://mechanic.pptaxi.ru/` | 200 OK, text/html, served from SPA | | `GET /manifest.webmanifest` | JSON with `"name": "Premium Механик"` | | `GET /api/v1/mechanic/healthz` (via mechanic.pptaxi.ru) | `{"status":"ok"}` | | `GET /api/v1/mechanic/me` (no auth) | 401 | | `GET /api/v1/mechanic/vehicles` (no auth) | 401 | | `GET /icons/icon-192.png` | 200 image/png, 2326 bytes | | `GET /sw.js` (service worker) | 200 text/javascript | | `GET /vehicles/12345` (SPA route) | 200 text/html (index.html fallback) | | `crm.pptaxi.ru/api/v1/mechanic/healthz` x5 | 5/5 ok (both blue+green serving) | **Remaining manual smoke (mobile, J3 step 8):** open `https://mechanic.pptaxi.ru` on Android Chrome / iOS Safari, login with test mechanic account, create inspection, snap photo, verify MinIO upload + thumbnail appears, verify "Add to Home Screen" works. *Not automated — you'll do that on a phone.* ## 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. | | 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}/.`; 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 `b37a068` — `generate_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 (`-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 `fc4060d` — `api/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 → ``) solving the ``-can't-send-Authorization problem. `CameraCapture` uses `` for max compat. `PhotoSlot` combines them. | | I | I4-I6: VehicleScheme + InspectionEditor + InspectionReview | ✅ DONE | commit `a428395` — `sedan-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}/.` | 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`, `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 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. ## 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) - [x] Backend `requirements.txt` already has `aiobotocore>=2.13.0`, `Pillow==10.4.0`, `pillow-heif==0.18.0`. - [x] Backend uses `Base.metadata.create_all` in lifespan (NOT Alembic) — new mechanic tables auto-create on container restart. **No migration step needed.** - [x] MinIO bucket `pp-inspections` exists with CORS configured (Phase A3, `ops/set-minio-cors.sh` already applied). - [x] Caddy fragment `ops/Caddyfile.fragment` ready in PremiumDriverApp repo. - [x] 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 head` — **both wrong for our setup**. We use Docker blue/green + `Base.metadata.create_all`. Correct steps: ```bash 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) ```bash # 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: ```sql 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=; SELECT id, side, damage_type, carried_over_from_id, resolved FROM mechanic_damage_markers WHERE inspection_id=; ``` 7. Verify in MinIO console (`https://s3-admin.pptaxi.ru`) bucket `pp-inspections`: see the `mechanic/inspections//.jpg` original + `-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 OK** ← **HERE** **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.