26 KiB
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), которое:
- Полностью заменяет вендорское приложение для сотрудников-механиков.
- Хранит данные осмотров в нашей PostgreSQL в PremiumCRM, не отправляя их к вендору.
- Поддерживает основные кейсы: съёмка фото авто, обводка повреждений на фото, метки повреждений с координатами, сравнение с прошлым осмотром при приёмке.
- Развёртывается без RuStore / App Store — деплой =
git push.
Долгосрочная цель: отвязать парк от 1С вендора, начав с самой ценной модели данных (осмотры с фото).
2. Цели и не-цели
Цели
- Делать осмотр авто: 4-8 фото по сторонам, метки и обводки повреждений на каждой фото.
- При приёмке машины обратно от водителя — показать прошлые повреждения для сравнения «было / стало».
- Все данные осмотра хранятся в PostgreSQL PremiumCRM (новая БД-схема), без участия вендора.
- Доступ для авторизованных сотрудников-механиков (через существующий auth PremiumCRM).
- Работает на 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:
-- Осмотр
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
- Permissions: добавить новое право
mechanic:inspectили ограничиться существующимdept=Тех. служба? — выбор на этапе implementation. - Storage: решено использовать MinIO, только MinIO без локального FS-дубля (разрешено отклониться от dual-write паттерна для тяжёлых медиа). Параметры: bucket
pp-inspections, endpoints3.pptaxi.ruHTTPS, креды в/opt/sites/taxi-dashboard/minio.env. Существующаяaiobotocore-интеграция переиспользуется. Остаётся операционно: настроить CORS на bucket для allowed origins (https://mechanic.pptaxi.ru,https://crm.pptaxi.ru, devhttp://localhost:5173) — задача в плане реализации. - Связь с CheckAuto: опционально показывать в карточке машины автоматические проверки водителя — после MVP.
- Импорт истории осмотров от вендора: через VendorBridge sub-project — после MVP.
12. Следующий шаг
После утверждения этой спеки:
- Создать подробный план реализации Фазы 1 через
writing-plansskill. - Сгенерировать миграции БД + scaffold бэкенд-модуля.
- Скаффолд React-проекта в
mechanic-pwa/frontend/с базовой страницей логина. - На итерациях по 2-3 экрана.