# Premium Механик — PWA Design Doc

- **Дата:** 2026-05-16
- **Статус:** Draft, ожидает review владельца
- **Владелец:** vladtechno@gmail.com (PremiumPark)
- **Родительский проект:** PremiumDriverApp
- **Экосистема:** Premium CRM (TaxiDashboard) на crm.pptaxi.ru
- **Предшественник:** реверс-разведка приложения «Механик» NaughtySoft (см. `01-static-analysis.md` + перехват трафика)

---

## 1. Контекст и цель

Сотрудники-механики PremiumPark сейчас работают через приложение «Механик» вендора NaughtySoft (`com.naughtysoft.ttc`, Xamarin/.NET). Все данные осмотров уходят в 1С вендора, доступ к ним из PremiumCRM невозможен без прохода через вендорские API.

Через разведку (см. `02-network-capture.md`) подтверждён формат backend-API вендора (REST на `api.ttcontrol.naughtysoft.ru`). Эндпоинты осмотров (`/api/vehicletechinspection/byvehicle`, `/api/vehiclestate/history`, `/api/image/<id>`, `/api/inspectionvideo`) подтверждают, что **модель данных осмотров с координатами повреждений и фото уже существует и нам полностью видна**.

**Цель проекта:** построить **собственное** приложение «Premium Механик» (PWA), которое:

1. Полностью заменяет вендорское приложение для сотрудников-механиков.
2. Хранит данные осмотров **в нашей PostgreSQL** в PremiumCRM, не отправляя их к вендору.
3. Поддерживает основные кейсы: съёмка фото авто, обводка повреждений на фото, метки повреждений с координатами, сравнение с прошлым осмотром при приёмке.
4. Развёртывается без RuStore / App Store — деплой = `git push`.

**Долгосрочная цель:** отвязать парк от 1С вендора, начав с самой ценной модели данных (осмотры с фото).

## 2. Цели и не-цели

### Цели

1. Делать осмотр авто: 4-8 фото по сторонам, метки и обводки повреждений на каждой фото.
2. При приёмке машины обратно от водителя — показать прошлые повреждения для сравнения «было / стало».
3. Все данные осмотра хранятся в PostgreSQL PremiumCRM (новая БД-схема), без участия вендора.
4. Доступ для авторизованных сотрудников-механиков (через существующий auth PremiumCRM).
5. Работает на Android-планшетах механиков, на iPad/iPhone, на десктопе — без раздельных сборок.

### Не-цели

- Не делаем приложение для водителя в рамках этого подпроекта (отдельная фаза).
- Не интегрируем фактическую интеграцию обратно в 1С вендора (наоборот — уходим от неё).
- Не делаем нативные APK / IPA в первой версии. Только PWA. Возможен Capacitor-fallback в фазе 3 при необходимости.
- Не делаем offline-first в MVP (механик работает с WiFi в парке). Service Worker — задел на будущее.
- Не делаем real-time чат / уведомления через push на iOS (сложно, неприоритетно).
- Не импортируем существующие осмотры из вендора в первой версии (отдельная задача миграции данных).

## 3. Технологический стек

### Frontend

| Слой | Выбор | Обоснование |
|---|---|---|
| Build/Dev | **Vite + TypeScript** | Быстрая итерация, типизация. Стек, уже знакомый команде. |
| UI Framework | **React 18** | Команда уже работает с React (SuperLanding на Astro + React islands). |
| UI Library | **shadcn/ui + Tailwind CSS** | Совпадает с design preferences (cream palette, Inter only, без жёлтого, без emoji). |
| State Client | **TanStack Query (React Query)** | Кэширование fetches, optimistic updates, retries. |
| Local State | **Zustand** | Минималистичный store без boilerplate. |
| Routing | **React Router 7** | Standard в React экосистеме. |
| Forms | **React Hook Form + Zod** | Типизированная валидация, лёгкий контроль. |
| Canvas/Annotation | **Konva.js + react-konva** | Лучший выбор для photo annotations: layers, touch, gestures, undo/redo. |
| HTTP | **Native fetch + ky или axios** | TBD на старте. |
| PWA | **vite-plugin-pwa** | Service Worker, web manifest, install prompt — из коробки. |
| QR-сканер | **html5-qrcode** | Для сканирования госномера-стикера машины. |

### Backend

| Слой | Выбор |
|---|---|
| Расширение | Новый модуль `app/mechanic/` в FastAPI PremiumCRM |
| Базовый URL | `/api/v1/mechanic/*` |
| ORM | SQLAlchemy (используется в TaxiDashboard) |
| Миграции | Alembic |
| Auth | Существующий PremiumCRM (JWT cookie + bcrypt PIN) — переиспользуем |
| Permissions | Существующая dept-based система permissions (см. memory: `project_taxidashboard_permissions`) |
| Файловое хранилище | **MinIO** на 100.64.0.12 (контейнер `taxi-minio`). Bucket **`pp-inspections`** (уже создан). Endpoint `s3.pptaxi.ru` (HTTPS), admin UI `s3-admin.pptaxi.ru`. Креды в `/opt/sites/taxi-dashboard/minio.env`. |
| S3 client | **`aiobotocore`** — переиспользуем существующую интеграцию из TaxiDashboard (она же используется для фото осмотров уже). |
| Upload-схема | **Pre-signed POST** — фронтенд льёт фото напрямую в MinIO, минуя FastAPI. Backend выдаёт presigned URL + поля. |
| Read-схема | Существующий nginx-proxy `/media/<key>` на VDS (уже работает для других фото TaxiDashboard) ИЛИ pre-signed GET с TTL 1 час. Решим на этапе implementation. |
| Image-processing | Pillow для thumbnails и EXIF strip — запускается celery-task'ом после `confirm` upload'а. |
| Дополнительно | **Только MinIO**, без локального дубля на FS (решение владельца). Расхождение с существующим dual-write-паттерном в TaxiDashboard оправдано тяжестью медиа осмотров (10-20 фото на осмотр) — забивать диск VDS дублями нецелесообразно. |

### Развёртывание

| Хост | Назначение |
|---|---|
| 100.64.0.12 (PremiumCRM VDS) | Backend + DB + статика фото |
| Caddy | Reverse proxy + TLS + raw уролы для фото |
| Поддомен | `mechanic.pptaxi.ru` либо `crm.pptaxi.ru/mechanic/` (TBD, см. open question) |

## 4. Модель данных

Новые таблицы в существующей `taxi_dashboard` PostgreSQL:

```sql
-- Осмотр
CREATE TABLE inspections (
    id BIGSERIAL PRIMARY KEY,
    vehicle_id BIGINT NOT NULL REFERENCES vehicles(id),
    type VARCHAR(32) NOT NULL,             -- 'handover'|'return'|'periodic'|'ad-hoc'
    performed_by INTEGER NOT NULL REFERENCES users(id),
    driver_id INTEGER REFERENCES users(id),
    started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    finished_at TIMESTAMPTZ,
    status VARCHAR(16) NOT NULL DEFAULT 'in_progress',  -- 'in_progress'|'completed'|'cancelled'
    notes TEXT,
    prev_inspection_id BIGINT REFERENCES inspections(id),  -- ссылка на предыдущий осмотр того же авто
    meta JSONB DEFAULT '{}'::jsonb
);

CREATE INDEX idx_inspections_vehicle_started ON inspections(vehicle_id, started_at DESC);

-- Фото осмотра
CREATE TABLE inspection_photos (
    id BIGSERIAL PRIMARY KEY,
    inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
    side VARCHAR(16) NOT NULL,             -- 'front'|'rear'|'left'|'right'|'interior'|'vin'|'odometer'|'wheel-fl'|'wheel-fr'|'wheel-rl'|'wheel-rr'|'free'
    slot_index INT DEFAULT 0,
    storage_key TEXT NOT NULL,             -- путь относительно /opt/premiumcrm/uploads/
    annotated_key TEXT,                    -- путь к фото с canvas-обводкой (опц.)
    thumb_key TEXT,
    width INT,
    height INT,
    bytes BIGINT,
    taken_at TIMESTAMPTZ DEFAULT NOW(),
    display_order INT DEFAULT 0
);

-- Метка повреждения (на фото и/или на схеме авто)
CREATE TABLE damage_markers (
    id BIGSERIAL PRIMARY KEY,
    inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
    photo_id BIGINT REFERENCES inspection_photos(id),  -- NULL если только на схеме
    side VARCHAR(16),                      -- сторона авто
    x DECIMAL(6,4),                        -- normalized 0..1 в координатах фото или схемы
    y DECIMAL(6,4),
    polygon JSONB,                         -- опционально: координаты обведённого контура [{x,y}, ...]
    damage_type VARCHAR(32),               -- 'scratch'|'dent'|'paint'|'crack'|'missing'|'rust'|'glass'
    severity VARCHAR(16),                  -- 'cosmetic'|'minor'|'moderate'|'severe'
    description TEXT,
    carried_over_from_id BIGINT REFERENCES damage_markers(id),  -- если унаследовано от прошлого осмотра
    resolved BOOLEAN DEFAULT FALSE,        -- было ли устранено
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_damage_markers_inspection ON damage_markers(inspection_id);
```

**Ключевая идея для дифа «было/стало»:**

- При создании return-inspection backend копирует все `damage_markers` из `prev_inspection_id` со ссылкой `carried_over_from_id`. Механик может либо подтвердить «есть» (оставить), либо отметить «устранено» (`resolved=true`), либо добавить новые маркеры. На фронте отображается две группы: «было раньше» (полупрозрачные) + «новые» (яркие).

## 5. API контракт

Все endpoints под `/api/v1/mechanic/`. Auth — через существующий JWT cookie PremiumCRM. Permission gate: `mechanic:inspect` (новое право в матрице, добавляется в модуль permissions).

### Машины

```
GET /vehicles?q=<гос-номер|VIN>&limit=20
    → [{id, license_plate, vin, make, model, year, current_driver, last_inspection_at}]

GET /vehicles/{id}
    → детали авто + список последних осмотров

GET /vehicles/{id}/last-inspection
    → последний завершённый осмотр (для дифа при приёмке)
```

### Осмотры

```
POST /inspections
    body: {vehicle_id, type, driver_id?}
    → {id, ...}  создан в статусе in_progress; если type='return', копирует markers из prev

GET /inspections/{id}
    → полная карточка с photos + markers + meta

PATCH /inspections/{id}
    body: {status?, notes?, finished_at?}

POST /inspections/{id}/photos/upload-url
    body: {side, slot_index, content_type, size}
    → {photo_id, presigned_url, fields, storage_key, expires_at}
    # фронт льёт сам файл напрямую в MinIO через presigned_url

POST /photos/{photo_id}/confirm
    body: {actual_size?, width?, height?}
    → {id, storage_key, thumb_key, status: 'processing'|'ready'}
    # после confirm backend ставит celery-task на thumbnail и EXIF strip

POST /inspections/{id}/markers
    body: {photo_id?, side, x, y, polygon?, damage_type, severity, description}
    → {id, ...}

PATCH /markers/{id}
    body: {damage_type?, severity?, description?, polygon?, resolved?}

DELETE /markers/{id}

POST /photos/{id}/annotated/upload-url
    body: {content_type, size}
    → {presigned_url, fields, storage_key, expires_at}
    # аналогично: фронт льёт annotated-версию напрямую в MinIO

POST /photos/{id}/annotated/confirm
    → {id, annotated_key, status: 'ready'}
```

### Чтение фото

```
GET /photos/{id}             → 302 Redirect на pre-signed S3 URL (TTL 1 час)
GET /photos/{id}/thumb       → 302 Redirect на pre-signed S3 URL thumbnail
GET /photos/{id}/annotated   → 302 Redirect на pre-signed S3 URL annotated
```

### Меня (текущий механик)

```
GET /me
    → {id, name, dept, permissions: [...], avatar_url}
```

## 6. UI экраны и поток

### Экран 1. Login
PIN-вход в PremiumCRM (как уже работает в TaxiDashboard).

### Экран 2. Главная — выбор машины
- Поле поиска по госномеру/VIN
- Кнопка «Сканировать QR» (стикер на лобовом стекле машины с госномером)
- Список последних 10 машин, с которыми работал этот механик
- На каждой карточке: гос-номер, модель, водитель, дата последнего осмотра

### Экран 3. Карточка авто
- Резюме (гос-номер, VIN, модель, цвет, год)
- Текущий водитель (если в аренде)
- **Кнопки:** «Передача водителю» (handover) | «Приёмка от водителя» (return) | «Плановый осмотр» (periodic) | «Свободный осмотр» (ad-hoc)
- **Прошлые осмотры** — лента (превью фото + метки)

### Экран 4. Новый осмотр — список «слотов» фото
Карточка-grid: 4 обязательных + 4 опциональных:
- Перед, Зад, Левый, Правый (обязательные)
- VIN, Одометр, Салон, Свободное фото
- Каждый слот: пустой → камера UI; заполнен → миниатюра + индикатор количества меток
- В режиме `return` каждая карточка слота показывает «было N меток, добавь новые / подтверди / отметь устранённые»

### Экран 5. Камера
- Полноэкранный preview (через `getUserMedia()` если поддерживается, иначе `<input type="file" capture="environment">`)
- Overlay-направляющая под текущий слот (силуэт стороны авто для guidance)
- Снять → preview → принять/перенять

### Экран 6a. Vehicle Scheme (zone-навигация)

- Интерактивная SVG-схема авто с тремя проекциями: top-down (вид сверху), боковая (left/right), интерьер
- Каждая зона (бампер, капот, дверь, крыша, диск, лобовое и т.д.) — кликабельный hot-spot SVG `<path>`
- При тапе на зону → переход в Экран 5 (Камера) с предзаполненным `side`/`slot_index`
- Поверх схемы — полупрозрачные метки прошлых повреждений (для return-режима): красные точки на тех зонах, где были повреждения
- Прогресс осмотра: подсветка зон, у которых уже есть фото (зелёный outline)

### Экран 6b. Annotation Editor (главная фишка)
- Фото на весь экран, поверх — Konva canvas
- Tool panel:
  - 🔘 **Маркер** — тап создаёт точку-метку
  - ✏️ **Обводка** — рисуем замкнутый контур (полигон) вокруг повреждения
  - ↩️ Undo / 🗑️ Erase
  - 🎨 Тип/severity выбор (4 типа × 4 severity)
- При тапе на маркер — modal с типом, severity, описанием
- В `return`-режиме старые маркеры показываются полупрозрачными; тап → «есть/устранено»

### Экран 7. Обзор осмотра
- Карточка-grid всех фото + список всех маркеров + общие notes
- Кнопка «Завершить осмотр» → finished_at, status=completed

### Экран 8. Подтверждение (опц.)
- Подпись водителя (canvas) — опционально
- Сохранение, переход на главную

## 7. Архитектура frontend

```
src/
├── api/
│   ├── client.ts            # настройка fetch/axios + auth interceptor
│   ├── inspections.ts       # endpoints через TanStack Query
│   ├── vehicles.ts
│   └── photos.ts
├── components/
│   ├── ui/                  # shadcn-сгенерированные
│   ├── camera/
│   │   ├── CameraCapture.tsx
│   │   └── PhotoSlot.tsx
│   ├── annotation/
│   │   ├── AnnotationCanvas.tsx     # Konva-based
│   │   ├── ToolPalette.tsx
│   │   └── MarkerModal.tsx
│   ├── vehicle-scheme/
│   │   ├── VehicleScheme.tsx        # SVG interactive scheme
│   │   ├── schemes/                 # SVG-файлы по типу кузова (sedan, hatch, suv)
│   │   │   ├── sedan-top.svg
│   │   │   ├── sedan-side.svg
│   │   │   └── interior.svg
│   │   └── HotSpot.tsx              # переиспользуемая зона-кнопка
│   └── shared/
├── pages/
│   ├── Login.tsx
│   ├── Home.tsx
│   ├── VehicleCard.tsx
│   ├── InspectionStart.tsx
│   ├── InspectionEditor.tsx
│   └── InspectionReview.tsx
├── store/                   # Zustand store(s)
│   ├── inspectionDraft.ts
│   └── auth.ts
├── lib/
│   ├── pwa.ts               # SW registration
│   └── coords.ts            # norm/denorm coords
├── App.tsx
└── main.tsx
```

## 8. Архитектура backend (модуль в PremiumCRM)

```
app/mechanic/
├── __init__.py
├── router.py                # /api/v1/mechanic/ роуты
├── models.py                # SQLAlchemy: Inspection, InspectionPhoto, DamageMarker
├── schemas.py               # Pydantic
├── service.py               # бизнес-логика (start_inspection, finalize, copy_markers, ...)
├── storage.py               # сохранение файлов на VDS + thumbnails
├── deps.py                  # auth + permission deps
└── migrations/
    └── 20260516_mechanic_init.py    # Alembic
```

## 9. Фазы реализации

### Фаза 1. MVP — Skeleton (3-4 нед)
Только основной flow без annotation, для первичного теста UX:

- Backend: модуль `mechanic`, миграции, endpoints вэхикулов и осмотров, фото upload (без annotation)
- Frontend: Login → Home → Vehicle → Inspection (создать, выбрать тип, добавить фото в 4 слота, добавить просто маркер-точку без сложной обводки) → Review → Complete
- Дифф с прошлым: список маркеров прошлого осмотра, чекбоксы «есть/устранено»

### Фаза 2. Annotation Editor (+2-3 нед)
- Konva-based canvas-overlay с инструментами обводки (polygon)
- Сохранение annotated-фото отдельным URL
- Старые маркеры/обводки полупрозрачно поверх новой фотографии для сравнения

### Фаза 3. Polish (+1-2 нед)
- Service Worker + offline queue (фото снимаются на устройство, загружаются по WiFi)
- QR-сканер стикеров
- Push-уведомления для админа (через web push с FCM-фолбэком или Telegram-бота)
- Дашборд статистики в админ-части PremiumCRM

### Фаза 4 (опционально). Capacitor wrapper
- Если в production столкнёмся с ограничениями iOS Safari camera или offline
- Тот же codebase → native APK + IPA
- ~1-2 недели

## 10. Связь с разведкой

- Endpoints, обнаруженные у вендора (`api.ttcontrol.naughtysoft.ru/api/vehicletechinspection/byvehicle` и пр.), могут быть использованы **отдельно** для **миграции данных** — одноразовый импорт прошлых осмотров вендора в нашу базу. Это отдельный sub-project (не входит в MVP).
- В первой версии работаем только с НОВЫМИ осмотрами, создаваемыми в PremiumMechanic — старые остаются в системе вендора и используются для справки до момента миграции.

## 11. Решения и открытые вопросы

### Решения (по итогам ревью владельца)

| # | Решение |
|---|---|
| Поддомен | **`mechanic.pptaxi.ru`** (отдельный поддомен). DNS уже настроен владельцем. На VDS 100.64.0.12 в Caddyfile добавляется блок с TLS через ACME. |
| Подпись водителя | **Не нужна** в MVP. Из спеки исключена. |
| 2D-схема авто | **Нужна обязательно с MVP.** Делаем SVG-схему сами: top-down + боковая проекция + интерьер. Не привязываемся к ассетам вендора (в APK schemes как раздельных файлов нет, у них всё через WebView + либо embedded в Xamarin DLL, либо отдельная фотография крупным планом). У нас SVG-первый подход с zone-based hot-spots. |

### Замечания по разведке вендорского редактора

- Layout `vehicleinspectiondetaildrawinglayout.xml` подтверждает: вендор использует `<android.webkit.WebView>` для редактора рисования повреждений. То есть **наша PWA-стратегия = технически тот же подход, что у вендора**, только без обёртки Xamarin.
- Заголовок WebView в layout = «Левая дверь» — у вендора **zone-based** детализация (кнопка → крупный план зоны → редактирование).
- ObjectID формата `660e7abfd13231f1dd6e8a4d` в `vehicleId` доказывает, что у вендора на бэке **MongoDB**, а не 1С. Имя `taksi.0nalog.com:1703` (виденное в Driver-приложении) — отдельный 1С-узел, не основной storage. Это означает: имитировать «1С-структуру» нам не надо — у вендора и так нет 1С на ключевом пути данных осмотров.

### Остающиеся open questions

1. **Permissions:** добавить новое право `mechanic:inspect` или ограничиться существующим `dept=Тех. служба`? — выбор на этапе implementation.
2. **Storage:** решено использовать **MinIO**, **только** MinIO без локального FS-дубля (разрешено отклониться от dual-write паттерна для тяжёлых медиа). Параметры: bucket `pp-inspections`, endpoint `s3.pptaxi.ru` HTTPS, креды в `/opt/sites/taxi-dashboard/minio.env`. Существующая `aiobotocore`-интеграция переиспользуется. Остаётся **операционно**: настроить CORS на bucket для allowed origins (`https://mechanic.pptaxi.ru`, `https://crm.pptaxi.ru`, dev `http://localhost:5173`) — задача в плане реализации.
3. **Связь с CheckAuto:** опционально показывать в карточке машины автоматические проверки водителя — после MVP.
4. **Импорт истории осмотров от вендора:** через VendorBridge sub-project — после MVP.

## 12. Следующий шаг

После утверждения этой спеки:

1. Создать подробный план реализации Фазы 1 через `writing-plans` skill.
2. Сгенерировать миграции БД + scaffold бэкенд-модуля.
3. Скаффолд React-проекта в `mechanic-pwa/frontend/` с базовой страницей логина.
4. На итерациях по 2-3 экрана.
