diff --git a/.superpowers/sdd/di2-embed/progress.md b/.superpowers/sdd/di2-embed/progress.md deleted file mode 100644 index e1d1c6f..0000000 --- a/.superpowers/sdd/di2-embed/progress.md +++ /dev/null @@ -1,21 +0,0 @@ -# SDD ledger — plan: внедрение Data-Insight2 в iistwin (субапп /di2) - -BASE (start): a60a75e - -## Решения (Rulings) -- R1: DI2 внедряется целиком «как есть» (пользователь, 2026-09-05): отдельный SPA-субапп на `/di2` + серверные роуты DI2 регистрируются ДО старого модуля финансов и перехватывают общие пути (/api/income/*, /api/debt/* и т.д.). Старый модуль НЕ удаляем до сравнения пользователем. Стоит ошибки: дублирование 68 роутов — лечится удалением старого модуля позже. -- R2: ai-agent.ts (Telegram-поллинг бот) НЕ переносим. Вместо него — `ai-config.ts` с чистыми функциями IO (loadAiConfig/saveAiConfig/loadAiPlanningConfig/saveAiPlanningConfig/getAiLog/saveLogEntry), вырезанными из ai-agent.ts. Роуты /api/ai/toggle и /api/ai/status удаляем (ссылаются на polling), autoStartIfEnabled() не вызываем. Остальное AI (config, planning-config/toggle, log, models, planning/ai-analyze) сохраняем. Стоит: кнопки «вкл бота» в настройках AI DI2 не будет. -- R3: startAuditScheduler() из DI2 не запускаем (у iistwin уже есть свой аудит-шедулер server/finance/audit.ts; двойные тяжёлые запросы во внешнюю БД не нужны). Ручной /api/audit/run и /api/audit/history DI2 сохраняем. Стоит: расписание аудита DI2 не активно — при желании включить позже одной строкой. -- R4: compression не переносим (не зависимость iistwin, пользы мало за reverse proxy). Стоит: ничего. -- R5: Вместо отдельного пула DI2 db-client делегирует к пулу iistwin `server/finance/db-client.ts` (один пул, тот же файл data/db-config.json). Стоит ошибки в делегировании — проверить тестом подключения. -- R6: Google-redirect DI2 переписать с REPLIT_DOMAINS на env APP_BASE_URL || https://iistwin.ru (путь /api/auth/google/callback оставить, конфликтов нет). - -## Preflight scan -- Task 1 (server) и Task 2 (client) трогают общий package.json (deps node-cron / scripts build) — поручаем оба изменения Task 1, Task 2 не трогает package.json... УТОЧНЕНИЕ: build-скрипт di2 логичнее в Task 2; конфликт один (строка scripts.build) — правило: Task 2 редактирует scripts.build, Task 1 только добавляет node-cron в dependencies. Порядок: Task 1 → Task 2 (resume агента). -- Проверка spec возможна только после обоих тасков (build проходит только с клиентом). Ревью единое по всему диапазону BASE..HEAD. - -## Заметки -- 68 общих путей DI2/iistwin — DI2 регистрируется первым, старые становятся теневыми (намеренно). -- client-di2 вне include корневого tsconfig → отдельный tsconfig, check-скрипт `tsc -p client-di2/tsconfig.json`. -- wouter 3.10 поддерживает `` — base="/di2". -- DI2 tsconfig идентичен корневому iistwin по строгости (strict: true) — копируется с заменой путей алиасов. diff --git a/.superpowers/sdd/di2-embed/task-1-brief.md b/.superpowers/sdd/di2-embed/task-1-brief.md deleted file mode 100644 index fbc48a4..0000000 --- a/.superpowers/sdd/di2-embed/task-1-brief.md +++ /dev/null @@ -1,59 +0,0 @@ -# Task 1 — Серверная интеграция Data-Insight2 - -Цель: перенести серверную часть DI2 (`C:\1Ai\1iistwin\tmp\Data-Insight2\server`) в `server/finance-di2/` репозитория `C:\1Ai\1iistwin\1Project`, обёрнуть в авторизацию iistwin и зарегистрировать роуты ДО старого модуля финансов. Клиент НЕ входит в задачу (Task 2), но добавь зависимость `node-cron` (DI2 audit-agent.ts импортирует cron вверху файла; планировщик мы не запускаем, но импорт должен резолвиться). НЕ меняй `scripts.build` в package.json (это Task 2). - -## Источник (только чтение) -- `C:\1Ai\1iistwin\tmp\Data-Insight2\server\`: routes.ts (6387 строк), cache.ts, table-config.ts, override-tables.ts, google-auth.ts, audit-agent.ts (477 строк), db-client.ts, storage.ts (5 строк). -- `C:\1Ai\1iistwin\tmp\Data-Insight2\shared\schema.ts` (366 строк, чистые типы/константы, без drizzle). - -## Что создать в 1Project - -1. **`server/finance-di2/`** — копии файлов DI2 с правками: - - `schema.ts` ← копия `tmp/Data-Insight2/shared/schema.ts` (типы и константы). - - `db-client.ts` — НЕ копировать DI2-версию. Написать обёртку, которая импортирует из `../finance/db-client` (iistwin): `query, testConnection, getConfig, setConfig, isConnectionError` и экспортирует их же + `getPoolClient` (проверь, что `server/finance/db-client.ts` экспортирует все эти имена; если `getPoolClient` отсутствует — добавь его туда, а не дублируй пул: открой файл и посмотри, есть ли там pool — экспортируй существующий). Формат конфига (data/db-config.json) идентичен — это тот же код по происхождению. - - `cache.ts`, `table-config.ts`, `override-tables.ts` — копии без изменений (проверь их импорты: они должны ссылаться на `./db-client` и `./schema` — исправь алиасы). - - `google-auth.ts` — копия с правкой `getRedirectUri()`: вместо `process.env.REPLIT_DOMAINS` использовать `process.env.APP_BASE_URL || "https://iistwin.ru"`, redirect `${APP_BASE_URL}/api/auth/google/callback`. Протокол всегда https. - - `ai-config.ts` — НОВЫЙ файл: вырезать из `tmp/Data-Insight2/server/ai-agent.ts` только чистые функции IO без Telegram/поллинга: `loadAiConfig`, `saveAiConfig`, `loadAiPlanningConfig`, `saveAiPlanningConfig` (файлы data/ai-config.json, data/ai-planning-config.json), а также `getAiLog`, `saveLogEntry` (посмотри их реализацию в ai-agent.ts — обычно append в json/jsonl; перенеси как есть, включая нужные интерфейсы AiConfig/AiPlanningConfig). Без cron, без fetch, без Telegram. - - `audit-agent.ts` — копия, но удалить `startAuditScheduler` и его вызовы (cronJob и импорт node-cron не нужны, если импорт только для этой функции — проверь: если cron используется только в startAuditScheduler, удали импорт и функцию целиком; тогда node-cron в package.json НЕ нужен — не добавляй). Оставить `runIntegrityAudit`, `getAuditHistory`. - - `routes.ts` — копия с такими правками: - - Импорт `dbConnectionSchema` с `./schema` (не `@shared/schema`). - - Удалить `import compression from "compression"` и `app.use(compression());` (строка ~1142). - - Строку 1371 `const {...} = await import("./ai-agent")` заменить на импорт из `./ai-config`: нужны только `loadAiConfig, saveAiConfig, loadAiPlanningConfig, saveAiPlanningConfig, getAiLog, saveLogEntry`. - - Удалить роуты `app.post("/api/ai/toggle", ...)` и `app.get("/api/ai/status", ...)` (используют startPolling/stopPolling/isPolling). - - Удалить вызов `autoStartIfEnabled();` (~строка 1508). - - В конце: удалить `startAuditScheduler();` (строка 6384), оставить `scheduleTreeRebuild();`. - - Проверь, что в routes.ts больше нет ссылок на ai-agent, compression, node-cron (grep). - - Проверь остальные импорты вверху routes.ts: `./storage` (5-строчный файл — перенеси тоже, если импортируется), `./override-tables`, `./table-config`, `./cache`, `./google-auth`, `./audit-agent` — все становятся `./xxx` внутри finance-di2 (уже так). - - `index.ts` — НОВЫЙ файл-обёртка `registerDi2Routes(app: Express)`: - - Создать `express.Router()`. - - Middleware на роутере: если `req.path` начинается с одного из префиксов DI2 — пропустить через `authenticateToken` затем `requirePermission('finance.manage')` (импорт из `../middleware/auth.middleware`), иначе `next()`. - - Префиксы: `/api/income`, `/api/income-categories`, `/api/expenses`, `/api/expense-categories`, `/api/debt`, `/api/dashboard`, `/api/profitability`, `/api/heatmap`, `/api/planning`, `/api/salary`, `/api/audit`, `/api/calculations`, `/api/connection`, `/api/db`, `/api/table-config`, `/api/table-settings`, `/api/overrides`, `/api/tags`, `/api/tag-assignments`, `/api/cache`, `/api/refresh-all`, `/api/admin`, `/api/auth/google`, `/api/ai`, `/api/telegram`. - - Затем вызвать DI2 `registerRoutes(httpServer, router as any)` — передав `null as any` вместо httpServer (он используется только в `return httpServer` в конце). Функция async — await. Затем `app.use(router)` — но НЕ сmount-префиксом, роуты абсолютные. - - ВАЖНО порядок middleware: `router.use(authGate)` должен стоять ПЕРЕД регистрацией роутов DI2, и все DI2-роуты регистрируются на этом же роутере. -2. **`server/routes/index.ts`**: добавить `await registerDi2Routes(app);` строкой НЕПОСРЕДСТВЕННО ПЕРЕД `await registerFinanceRoutes(app);` (строка 113). Импорт добавь рядом с импортом registerFinanceRoutes (строка 33). -3. **`server/index.ts`**: перед вызовом `serveStatic(app)` (или где там статика подключается в prod-ветке — найди по `serveStatic(`) добавить: - ```ts - // Data-Insight2 sub-app (финансы v2) - const di2Dist = path.resolve(import.meta.dirname, "public-di2"); - if (fs.existsSync(di2Dist)) { - app.use("/di2", express.static(di2Dist)); - app.use("/di2", (_req, res) => res.sendFile(path.resolve(di2Dist, "index.html"))); - } - ``` - (проверь, какие импорты path/fs уже есть в index.ts; позицию выбери так, чтобы это было ДО catch-all `serveStatic`, но ПОСЛЕ регистрации API-роутов). В dev-режиме (setupVite) тоже должен работать: setupVite вешает `app.use("*")` — проверь порядок: если setupVite вызывается в dev ПОСЛЕ нашего блока, то /di2/* уйдёт в main index.html. Чтобы dev тоже работал, добавь этот блок ПЕРЕД веткой `if (process.env.NODE_ENV === "development") setupVite(...) else serveStatic(app)` (найди эту ветку и вставь до неё). Проверь наличие fs/path импортов. -4. **package.json**: если audit-agent оставил импорт cron — добавь `node-cron` в dependencies (проверь версию из DI2 package.json). Если импорт удалён — не добавляй. `scripts.build` НЕ трогать. - -## Проверки (обязательно выполнить и приложить вывод) -1. `grep -rn "ai-agent\|compression\|REPLIT" server/finance-di2/` — пусто. -2. `npx tsc --noEmit` (корневой check) — без ошибок в server/finance-di2 (ошибки в других местах, если были до тебя, не считаются; проверь baseline сначала: `git stash` не нужен — просто сравни с ошибками на BASE, если есть новые — чини). -3. `npm run build` должен пройти (клиентская часть di2 ещё не создана — это нормально, build собирает только main client; сборка di2 будет в Task 2). Если build падает из-за твоих изменений — чини. -4. Быстрый smoke-тест сервера без Docker: запустить `npm run dev` не надо (трогает БД продакшена локально? нет — локальный dev стучится в локальную БД; НЕ запускай дев-сервер). Достаточно typecheck+build. - -## Контракт отчёта -Напиши файл `C:\1Ai\1iistwin\1Project\.superpowers\sdd\di2-embed\task-1-report.md`: что скопировано, какие правки, вывод grep-проверок, вывод tsc/build (последние строки), открытые вопросы. Верни: статус, список коммитов, одну строку по проверкам, опасения. - -## Жёсткие ограничения -- Код/комментарии — на русском (существующие DI2-комментарии не переводить обратно, оставь как есть). -- Не трогай `server/finance/*` кроме случая, когда нужно экспортировать getPoolClient (добавь экспорт существующего пула, не создавай второй пул). -- Не коммить изменения в tmp/Data-Insight2. -- Pre-push hook прогоняет check+build — коммить в main. diff --git a/.superpowers/sdd/di2-embed/task-1-report.md b/.superpowers/sdd/di2-embed/task-1-report.md deleted file mode 100644 index 929dde2..0000000 --- a/.superpowers/sdd/di2-embed/task-1-report.md +++ /dev/null @@ -1,41 +0,0 @@ -# Task 1 — Серверная интеграция Data-Insight2: отчёт - -## Статус: DONE - -## Что сделано - -### Новая папка `server/finance-di2/` -- `schema.ts` — копия `tmp/Data-Insight2/shared/schema.ts` (чистые типы/константы, zod). -- `db-client.ts` — **НЕ копия DI2**, а обёртка поверх `server/finance/db-client.ts` (iistwin): реэкспорт `query, testConnection, setConfig, isConnectionError, getPoolClient` + `getConfig()` с приведением к DI2-совместимой форме (iistwin-версия возвращает конфиг без пароля и может вернуть `null`; обёртка возвращает `DbConnectionConfig` с пустым паролем, как ожидает DI2-код). Второй пул не создаётся. -- `cache.ts`, `table-config.ts`, `override-tables.ts` — копии без изменений (алиасы `@shared/schema` → `./schema` исправлены sed'ом в table-config.ts и routes.ts; override-tables/cache и так используют `./db-client`). -- `google-auth.ts` — копия, `getRedirectUri()` переделан: `process.env.APP_BASE_URL || "https://iistwin.ru"`, redirect `${APP_BASE_URL}/api/auth/google/callback`, протокол всегда https. `REPLIT_DOMAINS` удалён. -- `ai-config.ts` — НОВЫЙ файл: чистое IO из DI2 `ai-agent.ts` — `loadAiConfig/saveAiConfig` (data/ai-config.json), `loadAiPlanningConfig/saveAiPlanningConfig` (data/ai-planning-config.json), `getAiLog/saveLogEntry` (data/ai-query-log.json) + интерфейсы `AiConfig/AiPlanningConfig/AiLogEntry` и дефолтные промпты. Без cron, fetch, Telegram. -- `audit-agent.ts` — копия без `import cron from "node-cron"`, без `cronJob` и без `startAuditScheduler()` (конец файла). Оставлены `runIntegrityAudit`, `getAuditHistory`. **node-cron в package.json НЕ добавлен** (импорт удалён целиком — cron использовался только в startAuditScheduler). -- `routes.ts` — копия (6387 → 6359 строк) с правками: - - удалён `import compression from "compression"` и `app.use(compression());`; - - `dbConnectionSchema` импортируется из `./schema`; - - динамический `await import("./ai-agent")` заменён статическим импортом `./ai-config` (только 6 IO-функций); - - из `POST /api/ai/config` удалены вызовы `startPolling()/stopPolling()` (использовали Telegram-поллинг); - - удалены роуты `POST /api/ai/toggle` и `GET /api/ai/status`; - - удалён вызов `autoStartIfEnabled();`; - - удалён вызов `startAuditScheduler();` в конце (оставлен `scheduleTreeRebuild();`); - - **фикс предсуществующего бага DI2**: `buildGlobalExclusionConditions(incParams)` в `/api/profitability` и соседнем роуте вызывались без первого аргумента `cats` — под tsx в DI2 это молча возвращало `[]` (исключения категорий в рентабельности не применялись). Добавлен `loadCategories()` первым аргументом (строки ~2974 и ~3080). Поведение изменилось: глобальные excluded-категории теперь реально исключаются из рентабельности — как и задумано. -- `index.ts` — НОВЫЙ файл-обёртка `registerDi2Routes(app)`: `express.Router()`, middleware auth-gate (`authenticateToken` → `requirePermission('finance.manage')`) по 26 префиксам из брифа, стоящий ПЕРЕД регистрацией DI2-роутов на том же роутере; `await registerRoutes(null as any, router as any)`; `app.use(router)` без mount-префикса. - -### Изменения вне finance-di2 -- `server/finance/db-client.ts` — добавлен экспорт `isConnectionError` (существующий код DI2; новый пул не создавался). -- `server/routes/index.ts` — импорт `registerDi2Routes` + `await registerDi2Routes(app);` строкой НЕПОСРЕДСТВЕННО ПЕРЕД `await registerFinanceRoutes(app);`. -- `server/index.ts` — блок статики `/di2` из `dist/public-di2` (path/fs уже импортированы, `import.meta.dirname` — Node 20.11+, совместимо с esbuild ESM-бандлом) вставлен ПЕРЕД веткой `if (development) setupVite else serveStatic` (после регистрации API-роутов и `app.use('/apps', ...)`). В dev и prod /di2/* не уйдёт в main index.html. Папки dist/public-di2 пока нет — блок молча пропускается (сборку добавит Task 2). -- `package.json` — НЕ изменён (node-cron не нужен, scripts.build не тронут). - -## Проверки -1. `grep -rn "ai-agent\|compression\|REPLIT" server/finance-di2/` → пусто (exit 1). `grep -rn "node-cron" server/finance-di2/` → пусто. -2. `npx tsc --noEmit` → **чисто, без ошибок** (единственные 2 ошибки при первом прогоне — предсуществующий баг DI2 с buildGlobalExclusionConditions — исправлены). -3. `npm run build` → **прошёл**: vite build + esbuild `dist\index.js 2.7mb` (DI2-роуты в бандле: grep `api/profitability|api/refresh-all|api/auth/google/callback` → 9 вхождений). -4. Дев-сервер не запускался (по брифу достаточно typecheck+build). - -## Открытые вопросы / опасения -- **Дублирование путей со старым модулем финансов**: DI2 регистрируется ПЕРЕД `registerFinanceRoutes`, поэтому при совпадении путей (`/api/income`, `/api/connection`, `/api/db/tables`, `/api/audit/*` и др.) побеждают DI2-обработчики. Это осознанное решение брифа, но старые финансовые страницы (которые ходят на эти же пути) теперь получат DI2-формат ответов после деплоя — до появления клиента DI2 в Task 2 старый UI финансов на пересекающихся эндпоинтах может сломаться. Если это нежелательно до Task 2 — можно временно закомментировать регистрацию. -- `/api/auth/google/callback` теперь за auth-gate: Google-редирект без сессии iistwin вернёт 401. В DI2 он тоже требовал входа (Replit auth), поведение осознанное. -- `/api/telegram/*` в routes.ts оставлен как есть (конфиг + отправка через fetch, без поллинга) — бриф вырезал только поллинг из ai-agent. -- Проверка регистрации в рантайме (поднять сервер) не выполнялась — по брифу достаточно typecheck+build; реальный smoke логично сделать после Task 2 вместе с клиентом. diff --git a/.superpowers/sdd/di2-embed/task-2-report.md b/.superpowers/sdd/di2-embed/task-2-report.md deleted file mode 100644 index 6b9203b..0000000 --- a/.superpowers/sdd/di2-embed/task-2-report.md +++ /dev/null @@ -1,46 +0,0 @@ -# Task 2 — Клиентский субапп Data-Insight2 (client-di2): отчёт - -## Статус: DONE - -## Что создано - -### `client-di2/` — копия фронтенда DI2 -- `src/` (78 файлов: App.tsx, pages/, components/ + ui/, hooks/, lib/, index.css), `public/favicon.png`, `index.html` — как есть, кроме правок ниже. -- `shared/schema.ts` ← копия `tmp/Data-Insight2/shared/schema.ts`. -- `tsconfig.json` — копия DI2-tsconfig: `include: ["src/**/*", "shared/**/*"]`, `target: ES2022` (сначала забыл — tsc ругался на ES5, добавлено), `strict`, paths `@/*`/`@shared/*` → `./src`, `./shared`, `types: ["node", "vite/client"]`, `tsBuildInfoFile: ../node_modules/typescript/tsbuildinfo-di2` (бриф предлагал `./node_modules/...` — функционально то же, но без мусорной папки client-di2/node_modules). -- `tailwind.config.ts` ← копия DI2-конфига (у DI2 свои токены/радиусы, отличные от iistwin!). Content-глобы: `./client-di2/index.html`, `./client-di2/src/**/*` — **относительно cwd сборки** (npm-скрипты всегда из корня). Вариант `{ relative: true, files: [...] }` относительно конфига проверен — глобы не матчились, утилиты вырезались (css 11 kB, 0 классов `.flex/.grid/...`); с cwd-глобами — 88 kB, утилиты на месте. -- `src/index.css` — добавлена директива `@config "../tailwind.config.ts";` в начало: без неё postcss-плагин tailwindcss от cwd (корень репо) взял бы корневой `tailwind.config.ts` iistwin и вырезал бы стили DI2. -- `src/main.tsx` — обёртка `` (wouter 3.10, проп `base` поддерживается — проверено в types/router.d.ts и фактической версии node_modules). -- `index.html` — favicon сделан относительным (`href="favicon.png"`), чтобы не 404 на проде; script-src `/src/main.tsx` — дев-режим, vite переписывает при сборке. - -### `vite.config.di2.ts` (корень) -Минимальный конфиг: `react()` без replit-плагинов, `base: "/di2/"`, `root: client-di2`, alias `@`/`@shared` → client-di2, `build.outDir: dist/public-di2`, `emptyOutDir: true`. `__BUILD_SHA__` в client-di2 не используется (grep пуст) — define не добавлял. server/fs-секций нет. - -### `package.json` -- `build`: `vite build && vite build -c vite.config.di2.ts && esbuild server/index.ts ...` (main client → di2 → server bundle); -- `build:di2`, `check`: `tsc && tsc -p client-di2/tsconfig.json`, `check:di2`. -- **Новых dependencies НЕ добавлено** — все пакеты DI2 (wouter, @tanstack/react-query, radix-набор, recharts, cmdk, vaul, embla, input-otp, date-fns, xlsx, tailwindcss-animate, @tailwindcss/typography и пр.) уже есть в package.json iistwin (проверено скриптом). - -### Прочее -- `Dockerfile` — менять не нужно: `RUN npm run build` подхватит новую сборку, `COPY --from=builder /app/dist ./dist` скопирует и public-di2. -- `.gitignore` — `dist` уже игнорируется, не тронут. -- **Навигация финансов**: desktop-список — `client/src/components/finance/FinanceSidebar.tsx` (пункт после navItems, plain `` с иконкой ExternalLink, стили как у Link, поддержка collapsed); мобильная — `client/src/components/finance/FinanceBottomNav.tsx` (пункт в DropdownMenu «Разделы» после категорий, тот же паттерн ``). Финансовые страницы оборачиваются в FinanceLayout → ссылка появляется на всех экранах финансов. - -### Типовые фиксы предсуществующих ошибок DI2 (DI2 под tsx никогда не прогонял tsc) -- `calculations.tsx`: литеральные массивы полей → `as any[]` (2 места), `addCumulativeVat(grouped as any, ...)` — дженерик `groupMonthlyData` выводил T как `{period: string}`. -- `income-categories.tsx` / `expense-categories.tsx`: в типы mutationFn добавлен `ruleLogic?: "and" | "or"` (UI его отправляет, типы отставали). -- `income.tsx` / `expenses.tsx` / `expenses-overview.tsx`: `new Set(...)` для filterableSet (строка не assignable в Set литерального union). -Рантайм-поведение не менялось. - -## Проверки -1. `npm run check` — **чисто** (оба tsc: основной + client-di2). -2. `npm run build` — **проходит**: main client (24.9s) → di2 (6.7s, `dist/public-di2/index.html` 2.03 kB, css 88.3 kB, js 1363 kB) → esbuild `dist\index.js` 2.7mb. -3. `ls dist/public-di2/` — `index.html`, `assets/index-*.css`, `assets/index-*.js`, `favicon.png`. -4. `grep -o 'src="[^"]*"' dist/public-di2/index.html` → `src="/di2/assets/index-D5btRW62.js"`, css `href="/di2/assets/index-CLXURD96.css"` — префикс `/di2/` на месте. -5. Стыковка с Task 1: серверный бандл `dist/index.js` лежит рядом с `dist/public-di2` → `path.resolve(import.meta.dirname, "public-di2")` из Task 1 сходится. Статика `/di2` отдастся через express.static + fallback sendFile index.html (глубокие ссылки wouter работают). - -## Открытые вопросы / опасения -- Рантайм-smoke в браузере не выполнялся (по брифу достаточно check/build + контроль путей). Первый реальный прогон — на проде после деплоя; если DI2-страница откроется неавторизованной — auth-gate из Task 1 редиректа не делает (вернёт 401 JSON на API, SPA покажет ошибки БД) — стоит глянуть глазами. -- DI2-SPA шрифты грузит с Google Fonts (как в оригинале) — на проде зависит от доступности fonts.googleapis.com. -- CSS предупреждение «A PostCSS plugin did not pass the `from` option» при di2-сборке — косметическое, на выхлоп не влияет. -- Размер di2-бандла 1.36 МБ без код-сплиттинга (как в оригинале DI2) — принято как есть.