Gatehouse — testing guide¶
Інструкція для розробника / QA / нового користувача — як перевірити, що Gatehouse plugin зібраний правильно і всі 5 спринтів працюють end-to-end. Не плутати з user-guide.md (для оператора, без термінів JWT / WS / migrate).
0. Передумови — без цього нічого не запрацює¶
| Що | Як перевірити | Якщо ні |
|---|---|---|
Postgres dev-БД на localhost:5433 живий |
docker compose -f docker-compose.dev.yml ps postgres показує healthy |
docker compose -f docker-compose.dev.yml up -d postgres |
| Backend venv створений | ls backend/venv/Scripts/python.exe |
cd backend && python -m venv venv && venv/Scripts/python.exe -m pip install -r requirements.txt |
| Міграції Gatehouse + HRM + FortNet застосовані | python manage.py showmigrations gatehouse hrm fortnet_scud |
python manage.py migrate |
| AppStore-products створені | python manage.py shell -c "from shop.models import Product; print(Product.objects.filter(code='appGatehouse').exists())" → True |
python manage.py seed_shop |
| Seed-дані Gatehouse v5 створені | python manage.py shell -c "from gatehouse.models import GatePoint; print(GatePoint.objects.filter(tenant__code='demo').count())" → 4 |
python manage.py seed_gatehouse |
Frontend dev-сервер на localhost:5173 |
curl -sf http://localhost:5173 |
cd frontend/erp && npm run dev |
| EasyOCR встановлений (Sprint 2 + далі) | cd backend && venv/Scripts/python.exe -c "import easyocr; print('OK')" |
pip install easyocr (heavy ~600MB) |
| html5-qrcode у frontend (Sprint 3) | grep html5-qrcode frontend/erp/package.json |
cd frontend/erp && npm install html5-qrcode |
| device-bridge venv (Sprint 2) | ls device-bridge/venv/Scripts/python.exe |
cd device-bridge && python -m venv venv && venv/Scripts/python.exe -m pip install -r requirements.txt |
Швидкий старт усього стенду:
# Термінал 1 — backend + ERP (через VS Code task або cli)
node launcher-dev.js
# Термінал 2 — Gatehouse hardware bridge (Sprint 2+)
node launcher-bridge.js
Лаунчери опубліковані як VS Code tasks: Ctrl+Shift+P → Tasks: Run Task → Launcher: Dev / Launcher: Bridge.
Sprint 1 — Universal core (manual flow)¶
Що перевіряємо: усі 3 сценарії з §2 плану проходяться руками; net_weight рахується правильно; кнопки розблоковуються коли треба.
Кроки¶
- Відкрий
http://localhost:5173/gatehouse— побачиш SectionDashboard з 4 групами (Master Data,Transaction Data,Reports,Processes). - Master Data → Gate Points — список з 4 рядків:
gate_main(mixed)gate_personnel(personnel_turnstile)gate_weighbridge(weighing_station)checkpoint_yard_1(vehicle_gate)- Master Data → Equipment — 7 одиниць обладнання, з них видно колонку
driver(file_scale_generic,anpr_webcam_browser, ...). - Transaction Data → Weighing Tickets — 5+ записів від попередніх sprints (3 closed + 1 open + 1 needs_review зі Sprint 4 + 2 з Sprint 1).
- Transaction Data → Gate Events — 14+ подій з minulого тижня + з seed-flow.
- Клацни на будь-який ticket зі статусом
weighed_in(відкритий) або зайди уTransaction Data → Weighing Tickets → New: - Заповни прохідну
gate_weighbridge, дату. - Клацни «Створити талон» (синя кнопка справа).
- На URL →
/.../<id>форма перезавантажилась. - Кнопки
Брутто/Тара/Закрититепер активні (Tooltip-и на disabled-кнопках також показують підказки). - Введи у поле «Брутто (заїзд), kg»:
25400→ клацни кнопкуБрутто→ статус →weighed_in, у Stepper підсвічений 2-й крок. - Введи у поле «Тара (виїзд), kg»:
12000→ клацниТара→ статус →weighed_out, Stepper → крок 3. - Клацни
Закрити талон→ статус →closed, Stepper → крок 4. Перевір net_weight = 13400 kg у блоці «Нетто (computed)».
DoD ✅¶
- [ ] 4 GatePoint різних
purposeбачимо у списку. - [ ] Можна створити WeighingTicket з нуля → пройти Брутто → Тара → Закрити → net правильний.
- [ ] Tooltip-и на заблокованих кнопках пояснюють чому.
- [ ] Status Stepper підсвічує правильний крок.
Якщо щось зламалося¶
| Симптом | Причина / фікс |
|---|---|
entity not found у MasterDataPage |
seed не пройшов або EntityRegistry.register загубився — перезапусти backend |
| Кнопки disabled на існуючому ticket'і | Tooltip скаже чому: closed/posted ticket не редагується (як у фактичних бух-документах) |
| net_weight не оновлюється | Model.save() має recompute — якщо ні, переглянь weight_in/weight_out decimal precision |
Sprint 2 — Hardware bridge + WebSocket live-feed + EasyOCR webcam¶
Що перевіряємо: обладнання з'являється у Live-feed без перезавантаження; webcam ANPR розпізнає номер з фото зі смартфона з ≥1 з 3 спроб у нормальному світлі.
2.1 Bridge + scale-emulator¶
- Лаунчер:
node launcher-bridge.js(або VS Code →Launcher: Bridge). - У логах побачиш:
[bridge] starting device-bridge for gate_point_id=1, 2 equipment [bridge] [file_scale_generic] polling C:\weights\current.txt every 2.0s (deadband 1.0 kg) [bridge] [card_reader_emulator] watching C:\cards\latest.txt every 2.0s [scale] scale_emulator: writing to C:/weights/current.txt every 2s; pattern=in_loaded_out_empty - Кожні ~2 секунди bridge POSTить event у backend:
2.2 Live-feed на Dashboard¶
- Відкрий
http://localhost:5173/gatehouse/gatehouseprocesses/gatehouseDashboard. - У header справа — зелений Badge
WS онлайн. - Секція Live-feed обладнання показує події з
kind=scaleприблизно раз на 2-5 секунд (під час фаз ramp циклу 60с; у steady-фазах буде тиша черезdeadband_kg=1.0). - Картки прохідних показують
Обладнання: 3дляgate_main,2дляgate_weighbridge, тощо.
2.3 Webcam ANPR¶
http://localhost:5173/gatehouse/gatehouseprocesses/webcamAnprTester.- Дозволь доступ до камери.
- На смартфоні відкрий фото номера в форматі UA (
AA0000AA,AA00000, абоAAA0000). Можеш загрузити з Google Images "украинский номерной знак". - Обери
gate_mainу dropdown «Прохідна». - Клацни «Зробити фото» → 5-15 секунд (перший виклик завантажує EasyOCR-модель ~80 MB; наступні швидкі).
- У правій панелі —
plate_display(наприкладKA 1234 AB),confidence: 91%, на webcam-overlay видно зелений bbox з номером. - Клацни «Підтвердити» → toast
Подію надіслано: KA1234AB. - Перейди назад на Dashboard → у Live-feed побачиш свіжу
anpr_camera-event. - Включи Auto mode + Interval 2с + Min confidence 0.8 → тримай фото у кадрі → раз на 2с auto-push події (з дедупом на той самий plate).
DoD ✅¶
- [ ] Bridge стартує, тримає 2 driver-таски, drains outbox у backend.
- [ ] Indicator
WS онлайнзелений у dashboard. - [ ] Scale events з'являються у Live-feed з ~2с інтервалом.
- [ ] Webcam ANPR розпізнає plate ≥1 з 3 спроб у нормальному світлі.
- [ ] Auto mode деуплює — 5 секунд показу однієї plate = 1 push, не 50.
Якщо щось зламалося¶
| Симптом | Причина / фікс |
|---|---|
Bridge: 401 Unauthorized |
JWT протух (60-хв lifetime) — node launcher-bridge.js оновлює його при старті |
| Live-feed порожній | WS не зʼєднався — F12 console шукай WebSocket onclose code=4001/4002/4003; перевір чи в useAuthStore.user.tenant_id == path tenant_id |
| EasyOCR endpoint висить | Перший виклик — model download ~80MB. Логи backend покажуть Loading EasyOCR reader.... Чекай ~30 секунд, не клікай знову |
| EasyOCR 503 з install_hint | cd backend && venv/Scripts/python.exe -m pip install easyocr |
| OCR не розпізнає UA-номер | Спробуй: матовий папір з більшим plate, кращий світ, кадр під невеликим кутом. Sprint 5+ → можна замінити EasyOCR на FastALPR |
Sprint 3 — Vehicle gate scenario (ANPR + QR labels + checkpoint timeline)¶
Що перевіряємо: end-to-end від webcam-фото до закритого vehicle event з 4 checkpoint'ами у timeline.
Кроки¶
- Operator screen:
http://localhost:5173/gatehouse/gatehouseprocesses/gateOperator→ обериgate_main(mixed-purpose, тут vehicles). - У іншій вкладці —
webcamAnprTester(з Sprint 2). Покажи фото номера, що вже є у seed: AA1234BB(Volvo FH 25-тонник),AA5678CC(MAN),BC4567CC(Mercedes),BC1122DD(Renault),AA9988EE(Iveco).- Або номер, якого немає —
register_vehicle_gate_eventавто-створить новийVehicle(name='Авто XX0000XX', ownership_type='hired', description='Auto-created from ANPR gate detection.'). - У Operator screen з'явиться картка з:
- Великий ff="monospace" plate.
- Vehicle name + driver name (якщо є).
- QR UID (нащо вище, поки що пустий — буде заповнений після accept).
- Клацни
Прийняти + друк: - POST
/gate-events/{id}/accept/→ statusdraft → registered, qr_label_uid згенерувався. - У новій вкладці автоматично відкривається
vehicle-gate-pass.pdf(A6 landscape з plate + QR + meta). - PDF можна:
- Зберегти на локальний диск.
- Відобразити на іншому екрані (другий монітор / смартфон).
- Mobile checkpoint scanner: інша вкладка →
http://localhost:5173/gatehouse/gatehouseprocesses/checkpointScanner/checkpoint_yard_1: - Обери пост
checkpoint_yard_1. - Клацни
Старт сканера→ дозволь webcam. - Направ на QR з PDF (з кроку 6).
- Toast:
Чекпойнт записано: AA1234BB @ checkpoint_yard_1. - У правій панелі сканера — recent scan з кнопкою
Маршрут. - Клацни
Маршрут→ відкриваєтьсяvehicleTrace/<event_id>: - Header: plate великий,
НА ТЕРИТОРІЇпомаранчевий бейдж,На території: +35 хв. - Timeline:
Заїзд на територію(синій) →Чекпойнт checkpoint_yard_1(cyan) → ... з time deltas. - Повернись у webcam-tester → показ того ж номера, але цього разу натискай
--direction out(треба додати у URL? — або вкладка emulator з прапорцем direction).- Альтернативно — використай
python device-bridge/emulators/anpr_cli.py --plate AA1234BB --gate-point 1 --direction out --backend http://localhost:8000 --token <JWT>.
- Альтернативно — використай
- У VehicleTrace → автоматично з'явиться
Виїзд з території, статус →ЗАВЕРШЕНО(зелений).
DoD ✅¶
- [ ] ANPR-event для seeded vehicle → operator card з готовою vehicle info.
- [ ] Accept → PDF з QR відкривається у новій вкладці без помилок.
- [ ] PDF читається сканером (можеш відкрити PDF на смартфоні і направити webcam).
- [ ] Checkpoint scan додає GateCheckpoint у timeline.
- [ ] VehicleTrace показує open-visit → close переключає на
ЗАВЕРШЕНО.
Якщо щось зламалося¶
| Симптом | Причина / фікс |
|---|---|
| PDF дає 404 / 500 | PrintRegistry.get('vehicle-gate-pass') не знайшов template — перевір backend/gatehouse/print_templates/vehicle-gate-pass/base.html існує |
| PDF друкується, але без QR | qr_label_uid був пустим до accept — клацни Reject + Accept ще раз; у seeded events QR вже є |
| Сканер не відкриває камеру | HTTPS required для getUserMedia на не-localhost. Use localhost:5173 явно |
| Сканер не декодить QR | Утримуй стабільно 2-3с, очисти екран від відблисків. html5-qrcode логи у F12 console |
| Checkpoint scan дає 404 | QR з seeded events має qr_label_uid != ''; нові events отримують його лише після accept |
Sprint 4 — Weighing scenario (ANPR + scale correlator + GoodsReceipt bridge)¶
Що перевіряємо: повний end-to-end від webcam-фото до проведеного GoodsReceipt без жодного ручного вводу ваги.
4.1 Correlator pairing¶
- У launcher-bridge: scale-emulator має писати у
C:/weights/weighbridge.txt(за замовчуванням пише уcurrent.txtдляgate_main). Підправ через флаг або вручну запусти: - У
device-bridge.tomlдодай ще один equipment: Або просто змінь існуючий — поки що використаємо одну вагу. - Тимчасово зміни bridge
gate_point_idу toml на3(gate_weighbridge), щоб scale events ішли на вагову (correlator слухає тількиweighing_station/mixed). - Перезапусти
Launcher: Bridge. Bridge тепер постить scale events наgate_weighbridge. - Operator screen вагової:
http://localhost:5173/gatehouse/gatehouseprocesses/weighbridgeOperator→ обериgate_weighbridge. - У картці «Останнє зчитування ваги» через 2-5с з'явиться
25000.00 kg. - У іншій вкладці →
webcamAnprTester→ обери gategate_weighbridge→ фото номераAA1234BB. - Через ~2 секунди backend correlator знайде пару → у списку «Відкриті талони» з'явиться новий ticket з
weight_in=25000, статусweighed_in.
4.2 Tare + close¶
- Перепиши scale-emulator:
--value 12000(порожнє авто). - У webcam-tester → ще раз показ того ж номера → correlator знайшов open ticket →
weight_out=12000,direction_pattern=in_loaded_out_empty, статус →closed,net=13000 kg. - У списку «Відкриті талони» → ticket зник (бо closed). Він з'явився у блоці «Готові до приходу».
4.3 GR bridge¶
- Клацни на ticket у блоці «Готові до приходу» → відкриється форма з Stepper'ом на 4-му кроці (
Закрито). - Кнопка
Створити прихід (GoodsReceipt)активна (teal). Клацни. - У новій вкладці відкривається GoodsReceipt:
- 1 рядок: cargo_item (
Зерноз seed) ×13000 kg(або 13 t якщо unit=т). - FK
weighing_ticket→ ticket з кроку 1. - Повернись у форму ticket → Stepper → 5-й крок (
Прихід #N), kнопкаСтворити прихідзамінилась на teal Alert з посиланням.
DoD ✅¶
- [ ] Correlator пов'язує ANPR + scale у вікні 30с.
- [ ] WeighingTicket створюється/закривається без жодного ручного вводу ваги.
- [ ] direction_pattern auto-derives (брутто > тара →
in_loaded_out_empty). - [ ] GR створюється з рядком
cargo_item × net_weight, FK назад. - [ ] Idempotent — повторний клік
Створити прихідповертає той самий GR, не дубль.
Якщо щось зламалося¶
| Симптом | Причина / фікс |
|---|---|
| Correlator не пов'язує | Перевір gate_point.purpose ∈ ('weighing_station', 'mixed'). Buffer per-process → один Daphne worker, no Redis у Sprint 4 |
Ticket створюється з status needs_review |
Suspicious weight (weight≤0) або vehicle mismatch — подивись note поле; виправ у формі вручну |
| GR creation 400 | Перевір що ticket має cargo_item + partner + gate_point.organization |
| GR створюється, але порожній | Item.base_unit має бути unit, що знає система; default = kg. Якщо unit=т, qty конвертується в тонни |
Sprint 5 — Personnel turnstile + FortNet integration + ContainerHub bridge¶
Що перевіряємо: AttendanceLog приходить з обох каналів (own + FortNet); recompute_days_worked працює; ContainerHub bridge створює GateTransaction (на ContainerHub-tenant).
5.1 Власний канал турнікета¶
- Card emulator через лаунчер:
node launcher-bridge.js --card AABBCC01. - У логах:
- У DB перевір AttendanceLog з'явився:
Очікувано: 1+ рядок з
cd backend && venv/Scripts/python.exe -c " import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf.settings.development') django.setup() from hrm.models import AttendanceLog for l in AttendanceLog.objects.filter(tenant_id=6, source='gatehouse').order_by('-date'): print(l) "source='gatehouse',is_open=True.
5.2 FortNet канал¶
- Перевірка налаштувань:
http://localhost:5173/gatehouse/gatehouseprocesses/fortnetIntegration: csv_path:C:/eswf/backend/integrations/fortnet_scud/seed_data/access_log_2026_04.csv(з seed v5).last_synced_event_id: пустий (cursor ще не рухався).is_enabled:Увімкнено(зелений Badge).last_log: «ніколи».- Клацни «Синхронізувати зараз» → backend за ~1 секунду зчитає 50 рядків:
- notification:
Синхронізація: success— Created: 50, replays: 0, failed: 0. - Перевантаж сторінку → у блоці «Поточний стан»:
Курсор: 1050(event_id останнього рядка CSV).- Останній sync: щойно.
- У
Sync historyблок — рядок з 50 created. - Перевір AttendanceLog:
Очікувано: ~25 з
venv/Scripts/python.exe -c " import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf.settings.development') django.setup() from hrm.models import AttendanceLog print(f'Total: {AttendanceLog.objects.filter(tenant_id=6).count()}') print(f' source=gatehouse: {AttendanceLog.objects.filter(tenant_id=6, source=\"gatehouse\").count()}') print(f' source=fortnet: {AttendanceLog.objects.filter(tenant_id=6, source=\"fortnet\").count()}') "source='fortnet'(5 employees × 5 days з парами IN/OUT).
5.3 Інкрементальний pull¶
- Відкрий CSV вручну, додай новий рядок:
- На FortNet page → ще раз
Синхронізувати зараз. - Очікувано:
Created: 1, replays: 49 (попередні рядки з event_id ≤ 1050 пропущено), failed: 0. Cursor →1051.
5.4 Recompute days_worked¶
- Відкрий
http://localhost:5173/hr-manager/.../payrollSlips/(HRM). - Знайди slip за квітень для employee
EMP001(Іванов І.І.). - У формі slip → action
recompute-days-worked(або через API:POST /api/v1/hrm/payroll-slips/{id}/recompute-days-worked/). days_workedоновлюється до5(5 робочих днів з парами IN/OUT у CSV).
5.5 ContainerHub bridge¶
- Залежить від наявності встановленого
containerhubplugin (appType=module, paid). - Якщо встановлено + є
Containerз контейнеромMAEU1234567: - Через webcam-tester або anpr-cli →
payload.container_number = 'MAEU1234567'. - Backend ANPR-flow →
register_vehicle_gate_event→bridge_to_containerhub_if_installed→containerhub.GateTransaction.objects.create(container, vehicle, transaction_time, transaction_type, evidence). - Перевір у containerhub:
venv/Scripts/python.exe -c " import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf.settings.development') django.setup() from containerhub.models import GateTransaction for gt in GateTransaction.objects.order_by('-id')[:5]: print(gt.id, gt.container_id, gt.vehicle_id, gt.transaction_time, gt.transaction_type) "
DoD ✅¶
- [ ] Прикладали картку → AttendanceLog з
source='gatehouse'. - [ ] FortNet-sync читає CSV → AttendanceLog з
source='fortnet'. - [ ] Інкрементальний pull створює лише нові події (cursor рухається).
- [ ] PayrollSlip.days_worked перераховується з історії.
- [ ] (Опційно) ContainerHub bridge створює GateTransaction.
Якщо щось зламалося¶
| Симптом | Причина / фікс |
|---|---|
| Card emulator пише, але AttendanceLog не з'являється | Bridge на не-personnel gate_point — змінь gate_point_id у toml на 2 (gate_personnel) |
| FortNet sync 0 created | gate_mapping у settings не співпадає з реальним GatePoint.code — seed-default {'GATE-MAIN': 'gate_personnel'} |
FortNet sync failed > 0 |
F12 → last_log.failures показує конкретні рядки. Типово employee_not_found або gate_not_found |
recompute-days-worked повертає 0 |
Перевір AttendanceLog у тому ж period (year+month). Обидва in_at AND out_at потрібні для повного дня |
| ContainerHub bridge silent | Не встановлено plugin (apps.is_installed('containerhub') → False) — так і має бути |
Чеклист перед сдачею (прохід усіх 5 sprints)¶
[ ] Postgres up + migrations applied + seed v5 ran
[ ] Frontend dev (5173) + backend dev (8000) running
[ ] launcher-bridge running with scale + card emulators
S1: WeighingTicket manual flow ✅
[ ] Створив new ticket → Брутто → Тара → Закрити → net правильний
S2: Hardware bridge + WS + EasyOCR ✅
[ ] Live-feed scale events + WS online indicator
[ ] Webcam ANPR ≥1 з 3 спроб
S3: Vehicle gate + QR + checkpoint ✅
[ ] ANPR → Accept → PDF з QR
[ ] QR scan → checkpoint у timeline
[ ] Trace показує open → closed
S4: Weighing correlator + GR ✅
[ ] Correlator pair scale + ANPR
[ ] WeighingTicket weighed_in / closed без ручного вводу
[ ] GR створюється + back-link
S5: Personnel + FortNet ✅
[ ] Own SCUD: card emulator → AttendanceLog gatehouse
[ ] FortNet: 50 rows → 25 AttendanceLog fortnet
[ ] Incremental pull: cursor moves
[ ] recompute_days_worked = 5
[ ] tsc --noEmit clean
[ ] vitest dependencies.test.ts 24/24
[ ] manage.py check 0 issues