Files
mechanic-pwa/docs/superpowers/specs/2026-05-16-premium-mechanic-design.md

398 lines
26 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 Механик — 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 экрана.