160 lines
13 KiB
Markdown
160 lines
13 KiB
Markdown
# Premium Mechanic PWA — Implementation State
|
||
|
||
> Last updated: 2026-05-17 (end of session 2 — Phase B + Phase C done)
|
||
|
||
## What is done
|
||
|
||
| Phase | Task | Status | Artifact |
|
||
|---|---|---|---|
|
||
| 0 | Git infrastructure (Gitea + repos + feature branches) | ✅ DONE | repos exist, ssh keys configured |
|
||
| A | A1: Recon TaxiDashboard backend structure | ✅ DONE | `docs/backend-layout-notes.md` |
|
||
| A | A2: Caddy fragment for `mechanic.pptaxi.ru` | ✅ DONE | `ops/Caddyfile.fragment` |
|
||
| A | A3: MinIO CORS on `pp-inspections` bucket | ✅ DONE | `ops/set-minio-cors.sh`, applied, preflight 204 |
|
||
| A | A4: SQLAlchemy models (`MechanicInspection`, `MechanicInspectionPhoto`, `MechanicDamageMarker`) | ✅ DONE | commit `be0553e` on `feat/mechanic-mvp` |
|
||
| B | B1: Mechanic module skeleton + `/healthz` | ✅ DONE | commit `d2113a0` — flat `backend/app/api/mechanic.py` with `APIRouter(prefix="/v1/mechanic")`, registered in `app/main.py` with `prefix="/api"` → final URL `/api/v1/mechanic/healthz` |
|
||
| B | B2: SQLAlchemy models registered + tests | ✅ DONE | **already shipped in A4** (`be0553e`) — 38 metadata tests in `backend/tests/test_mechanic_models.py`, no extra B2 commit |
|
||
| B | B3: Pydantic schemas | ✅ DONE | commit `2739cc8` + fix `2fb978d` — 14 schemas + 6 Literals in `backend/app/api/mechanic_schemas.py`, 15 behavioral tests in `backend/tests/test_mechanic_schemas.py`; fix added `= None` defaults to `Optional[X]` fields and tightened `MarkerOut.damage_type`/`severity` to `DamageType`/`Severity` Literals |
|
||
| B | B4: Auth + DB session dependencies | ✅ DONE | commit `f0d7e83` — `backend/app/api/mechanic_deps.py` re-exports `get_db`+`get_current_user`, adds `require_mechanic` guard (admin OR member of any active `dept_type='svc'` department); 9 tests in `backend/tests/test_mechanic_deps.py` use fake-DB stubs (no real session needed)
|
||
| C | C1: `GET /me` endpoint | ✅ DONE | commit `a0bdcaa` + signature fix `1e5b822` — appended to `backend/app/api/mechanic.py`; returns `MeOut` from `require_mechanic` user; `permissions` is `["admin", "mechanic:inspect"]` for admins else `["mechanic:inspect"]`. Test fixtures (`client`, `override_mechanic`, `_fake_user`) in `backend/tests/test_mechanic_endpoints.py` use FastAPI `TestClient` + `app.dependency_overrides` per Option A — no real DB / no async. 6 endpoint tests. |
|
||
| C | C2: `GET /vehicles` list with `q`+`limit` search | ✅ DONE | commit `7be8783` — new `backend/app/api/mechanic_service.py` with `search_vehicles_with_last_inspection` (single aggregated subquery + outerjoin to avoid N+1 for last completed inspection per vehicle); endpoint in `mechanic.py`; new `VehicleListResponse` schema; 8 tests (6 endpoint + 2 service-stmt) with `_FakeSession.execute()` returning canned rows. Search hits plate (`CarV2.number`) + VIN only. |
|
||
| C | C3: `GET /vehicles/{id}` detail + bound /vehicles limit | ✅ DONE | commit `9109383` + style fix `90b8c66` — new `VehicleDetail(VehicleSummary)` + `InspectionSummary(_Base)` schemas; service `get_vehicle_by_id` + `recent_mechanic_inspections`; endpoint raises 404 when missing, `last_inspection_at` derived from first completed inspection in the list; applied `Query(20, ge=1, le=200)` bounds to C2's `/vehicles` limit param. 7 new tests (5 detail + 2 bounds). Service is now FastAPI-free (404 raise lives in endpoint, matching `automations.py` convention). |
|
||
|
||
**Branch state:** `feat/mechanic-mvp` at `90b8c66`. Full stack: `be0553e` (A4) → `d2113a0` (B1) → `2739cc8` + `2fb978d` (B3) → `f0d7e83` (B4) → `a0bdcaa` + `1e5b822` (C1) → `7be8783` (C2) → `9109383` + `90b8c66` (C3). All pushed to `origin` (Gitea).
|
||
|
||
**Test count on branch:** **83 mechanic tests pass** (38 models + 15 schemas + 9 deps + 21 endpoints across C1/C2/C3) in ~2s, no real DB needed.
|
||
|
||
**Live mechanic API surface (4 routes):**
|
||
- `GET /api/v1/mechanic/healthz` — liveness
|
||
- `GET /api/v1/mechanic/me` — current user + permissions
|
||
- `GET /api/v1/mechanic/vehicles?q=&limit=` — list with search (1≤limit≤200)
|
||
- `GET /api/v1/mechanic/vehicles/{vehicle_id}` — detail with recent inspections
|
||
|
||
## Where things live
|
||
|
||
### Repos (Gitea: `http://100.64.0.9:3001/tremble7681`)
|
||
|
||
| Repo | Local clone | Working branch |
|
||
|---|---|---|
|
||
| `mechanic-pwa` | `c:\NewProject\PremiumDriverApp\` | `feat/mechanic-mvp` |
|
||
| `taxi-dashboard` | `c:\NewProject\taxi-dashboard\` | `feat/mechanic-mvp` |
|
||
|
||
`taxi-dashboard` clone has two remotes: `origin` → Gitea, `vds` → SSH to `/opt/sites/taxi-dashboard/` on 100.64.0.12 (for pulling back-and-forth with prod).
|
||
|
||
### Reference paths (real, verified — see `docs/backend-layout-notes.md`)
|
||
|
||
- `Base`, `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.
|
||
|
||
## Next task
|
||
|
||
### **Task D1 — inspections CRUD foundation (create + list)**
|
||
|
||
(From `docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md` Phase D.)
|
||
|
||
**Working dir:** `c:\NewProject\taxi-dashboard` (branch `feat/mechanic-mvp`, HEAD `90b8c66`)
|
||
|
||
⚠️ **Heads up** — Phase D introduces **write operations** + the **carry-over markers** logic, which is qualitatively different from Phase C's read-only endpoints:
|
||
- POST/PATCH endpoints need request-body validation (Pydantic schemas exist; verify they match the plan's body shapes)
|
||
- Carry-over: when a new inspection is created for a vehicle, copy unresolved markers from the previous inspection (`prev_inspection_id` self-reference). Needs careful design.
|
||
- Idempotency / race conditions: two mechanics opening the same vehicle simultaneously
|
||
- Real persistence with `_FakeSession`: tests need to verify the right `add()` / `flush()` / `refresh()` calls happened. The current `_FakeSession` only mocks read operations — needs extension for write paths (`add(obj)`, `flush()`, `refresh(obj)` to seed an `id`).
|
||
|
||
**Recommendation for the next session:** before dispatching the D1 implementer, decide:
|
||
1. Whether the carry-over copy happens inside the create endpoint (simpler, atomic) or as a separate background step (e.g. Celery).
|
||
2. How `_FakeSession.add/flush/refresh` should behave for write-path tests (auto-assign id? track inserted objects?).
|
||
3. Whether Phase D is the right place to introduce a tiny real-DB conftest after all (re-evaluate Option C from earlier).
|
||
|
||
These three decisions are NOT trivial and warrant a brainstorming pass at the start of the next session before any implementer is dispatched.
|
||
|
||
Read the D1 task definition in the plan before deciding. Also check what other relevant Pydantic schemas exist that we built in B3 (`InspectionCreate`, `InspectionPatch`, `MarkerCreate`, `MarkerPatch`).
|
||
|
||
## How to resume in a new session
|
||
|
||
In a new Claude Code chat in `c:\NewProject\`, paste this prompt:
|
||
|
||
```
|
||
Продолжаем проект Premium Mechanic PWA.
|
||
|
||
Прочитай PremiumDriverApp/docs/superpowers/STATE.md — там полное состояние и следующая задача.
|
||
|
||
Phase C полностью завершён (C1–C3, последний коммит 90b8c66, 83/83 тестов зелёных).
|
||
Следующая фаза — Phase D (inspections CRUD с carry-over markers) из плана
|
||
PremiumDriverApp/docs/superpowers/plans/2026-05-16-premium-mechanic-phase1.md.
|
||
|
||
⚠️ Перед спавном D1 implementer-subagent сделай brainstorming pass по трём решениям
|
||
из секции "Next task" в STATE.md: (1) carry-over inline-vs-async, (2) _FakeSession
|
||
write-path extension, (3) пересмотр Option C для real-DB conftest в Phase D.
|
||
|
||
Используй superpowers:brainstorming, потом superpowers:subagent-driven-development.
|
||
```
|
||
|
||
That prompt is the **single entry point** for the next session — Claude will read STATE.md, ground itself, brainstorm the three Phase D architecture decisions, then dispatch the D1 implementer.
|
||
|
||
## Remaining roadmap (Phase 1 MVP)
|
||
|
||
- ~~**Phase B** (4 tasks)~~ ✅ done — module skeleton, models, schemas, auth deps
|
||
- ~~**Phase C** (3 tasks)~~ ✅ done — GET /me, GET /vehicles, GET /vehicles/{id}
|
||
- **Phase D** (3 tasks): inspections CRUD with carry-over markers logic ← **NEXT**
|
||
- **Phase E** (5 tasks): MinIO storage layer + presigned upload/confirm + 302 read + celery thumbnail
|
||
- **Phase F** (2 tasks): markers create/update/delete
|
||
- **Phase G** (5 tasks): Vite + React + Tailwind + shadcn + PWA scaffold
|
||
- **Phase H** (3 tasks): Login + Home + VehicleCard pages
|
||
- **Phase I** (6 tasks): CameraCapture + PhotoSlot + VehicleScheme + InspectionEditor + InspectionReview
|
||
- **Phase J** (4 tasks): e2e test + deploy backend + deploy frontend + smoke test
|
||
|
||
≈ 28 tasks left until MVP live on `mechanic.pptaxi.ru`.
|
||
|
||
## Known external dependencies
|
||
|
||
- MinIO `pp-inspections` bucket — CORS already configured, presigned uploads will work
|
||
- Caddy on 100.64.0.12 — fragment ready, will be wired during Phase J deploy
|
||
- Docker blue/green deployment on VDS — backend re-deployment will need a touch of `docker compose` (Phase J)
|
||
- Frontend deployment target: `/opt/sites/mechanic-pwa/dist/` on VDS (Phase J)
|
||
|
||
## Conventions reminder
|
||
|
||
- All new code in `feat/mechanic-mvp` branches in both repos
|
||
- All new tables use `mechanic_` prefix
|
||
- Don't touch existing `app/api/inspections.py` or `app/models/inspection.py` (vendor-imported)
|
||
- All new mechanic source lives in **flat files under `backend/app/api/`** with `mechanic_*` naming (`mechanic.py`, `mechanic_schemas.py`, `mechanic_deps.py`). No `app/mechanic/` subpackage — see "Plan-vs-reality overrides" table above.
|
||
- Router prefix is `/v1/mechanic`; `/api` is added by `include_router(..., prefix="/api")` in `main.py`. Final URLs always `/api/v1/mechanic/*`.
|
||
- Phase C+ endpoint tests: pending the test-infrastructure decision (Option A / B / C above). Do NOT introduce `pytest-asyncio`, `conftest.py`, or a real DB without first resolving that decision.
|
||
- `require_mechanic` allows admins + members of any active `Department` with `dept_type='svc'`. Use it as the default auth dep for all mechanic endpoints.
|