Files

385 lines
33 KiB
Markdown
Raw Permalink 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 (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}/<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 `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 (`<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 `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 → `<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 `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}/<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`, `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.
## 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=<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 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.