feat(pwa): вход для сотрудника, просмотр от лица водителя и вкладка «Отладка»

Приложение обкатывается на боевом контуре, поэтому интерфейс надо проверять на
живых данных — а живые данные лежат у настоящих водителей.

* «Вход для сотрудника» (/dev) — логин учётки CRM, поддержан второй шаг 2FA;
  дальше поиск водителя по ФИО/телефону/id и просмотр его экранов.
* Плашка «Смотрю как …» над всеми экранами: интерфейс в этом режиме неотличим
  от водительского, и чужой баланс легко принять за свой.
* Вкладка «Отладка» (/debug, водителю недоступна): смена водителя, driver_id,
  режим, срок токена, адрес API, коммит и время сборки, журнал последних
  40 ответивших запросов, сброс кэша сервис-воркера.
* Пополнение под просмотром идёт в учебный контур: экран /mock-pay/:orderId
  выбирает, что «ответит» банк, а настоящий экран оплаты разбирает эти статусы
  своим боевым кодом. Ни рубля, ни чека 54-ФЗ.

Заодно доезжает незакоммиченная работа прошлой сессии: дев-прокси в
vite.config.ts, его тест и разобранный .env.example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 13:15:02 +10:00
co-authored by Claude Opus 5
parent aff1a45006
commit ceec3db364
27 changed files with 1308 additions and 17 deletions
+19 -1
View File
@@ -1,7 +1,11 @@
import ky, { type KyInstance } from "ky";
import { pushEntry, shortPath } from "@/lib/reqlog";
import { useAuth } from "@/store/auth";
const API_BASE = import.meta.env.VITE_API_BASE ?? "https://crm.pptaxi.ru/api";
export const API_BASE = import.meta.env.VITE_API_BASE ?? "https://crm.pptaxi.ru/api";
/** Засечка времени на конкретный запрос. WeakMap — чтобы не держать Request'ы. */
const started = new WeakMap<Request, number>();
export const api: KyInstance = ky.create({
prefixUrl: API_BASE,
@@ -10,10 +14,24 @@ export const api: KyInstance = ky.create({
hooks: {
beforeRequest: [
(request) => {
started.set(request, performance.now());
const token = useAuth.getState().token;
if (token) request.headers.set("Authorization", `Bearer ${token}`);
},
],
// В журнал попадают ОТВЕТИВШИЕ запросы, включая 4xx/5xx. Запрос, оборвавшийся
// без ответа (таймаут, пропавшая сеть), ky сюда не приносит — на экране
// отладки об этом сказано прямо, чтобы пустая строка не читалась как «дошло».
afterResponse: [
(request, _options, response) => {
pushEntry({
method: request.method,
path: shortPath(request.url),
status: response.status,
ms: Math.round(performance.now() - (started.get(request) ?? performance.now())),
});
},
],
beforeError: [
(error) => {
if (error.response?.status === 401) useAuth.getState().clear();