sunprint-state/STATE.md

512 lines
56 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.

# Sunprint — рабочее состояние (STATE.md)
> **Что это.** Единый источник истины о статусе работ. Любой Claude (Code,
> claude.ai, новая модель, новый разработчик) читает этот файл первым делом
> и за 60 секунд понимает, где мы и куда идём.
>
> **Где живёт.** Корень репо `print-calc` + дубликат загружен в Project
> Knowledge проекта Sunprint в claude.ai. Один файл — одна правда. Не
> плодить параллельные «активный трек», «текущая задача», «next.md».
>
> **Кто обновляет.** Claude в любой среде — последним действием перед
> закрытием сессии. Если изменений по треку не было — просто ставит новую
> дату и пишет «без изменений».
---
**Обновлено:** 2026-04-30, Claude Code (Opus 4.7) — гигиена закрыта (push 0e53251 + observation v8.1 closed + snapshot rm), готовы к Dieline Фаза 1
**Длительность сессии:** в процессе (30.04: pre-flight ✅ → push 0e53251 → observation closed → snapshot+.htpasswd rm → правило #7 + accepted-risk фиксация)
**Предыдущая сессия:** 2026-04-29, Claude Code (Opus 4.7), ~6ч — Gitea CE задеплоен на git.suntask.ru, public+private repos, web_fetch workflow через commit-pinned URL
> **Заметка о merge:** этот файл собран из двух источников — базой взят архив `2026-04-29_09-04-38_STATE_post-basicauth.md` (md5 `acda575a5dc67402169f8586af2a0563`), сверху наложены только новые блоки сессии 29.04 (dieline-трек, два бэклог-ряда, урок №12, лог-строка, связанные документы по dieline, новый риск про устаревший Project Knowledge). Регрессия зафиксирована как риск ниже.
---
## 🎯 Активный трек
**Observation v8.1 закрыт ✅. Готов к Dieline Фаза 1.**
Все большие треки 27-29 апреля закрыты (БАГ колод v8.1, SSO bridge, Basic Auth снят, Gitea CE деплой). Гигиена 30.04: snapshot v8.1 удалён, .htpasswd убран, observation чек-лист пройден (0 ошибок за 48h, `2×200 + 1×400 + 0×500` на `/api/v1/calculate`), правило #7 (accepted risk) добавлено в Security политику.
**Следующий шаг:** запуск **Dieline Фаза 1** (локальный плейграунд в `demo.html`, расширение каталога под микрогофру, AST-парсер, валидация геометрии). Полный план — раздел «🆕 Трек: Конструктор коробок» ниже, ~1-2 недели локально.
Альтернативные кандидаты (если приоритет сместится):
- 🟡 `/admin/approvals` UI (~3ч, владельческий функционал)
- 🟢 v8.2 rebase под v8.1 pipeline (НДС 22% / шелкография / equipment)
---
## 🆕 Трек: Конструктор коробок (dieline generator)
**Тип задачи:** новая большая фича для print-calc. Параметрический генератор
развёрток упаковки. Интегрируется в print-calc как `_type: 'box_construction'`
по аналогии с `card_deck` (см. Уроки 1-4).
### Контекст
- Передан полный handoff-пакет от другой сессии: `README.md`, `ARCHITECTURE.md`,
`SCHEMA.md`, `ROADMAP.md`, `INTEGRATION.md`, `DECISIONS.md`, `catalog.json`,
`core.js`, `dxf-export.js`, `demo.html`. Все в Project Knowledge claude.ai.
- Текущее состояние: **v0.4**, vanilla JS без сборки, 7 конструкций в 4
категориях (Slotted/Sleeve/Tray/Insert), параметрический редактор, SVG +
DXF (R2000, слои Cut/Crease, дуги через bulge — production-ready).
- Сейчас цена коробки в Sunprint считается **вручную** — задача калькулятора
как раз перенести эту логику.
### Решение оператора (зафиксировано 2026-04-29)
1. **Целевой нишевый сегмент:** самосборные коробки из микрогофры (подарочные,
под визитки, мерч). НЕ полный FEFCO-200 — топ 5-15 конструкций под Sunprint.
2. **Архитектура:** путь Б — встроить в print-calc как новый тип продукта.
3. **Связан с расчётом цены:** да, ожидается в ближайшее время.
4. **Режим работы:** «локально → потом часть сайта». Фазируем (см. ниже).
### Фаза 1 — локальный плейграунд (1-2 недели)
Без касания print-calc, без БД, без сервера. Code работает в `demo.html`
на машине оператора:
- **Расширение каталога** до топ-N конструкций под микрогофру (сначала 3-5,
потом 8-12). Каждая верифицируется по правилу ROADMAP: распечатать 1:1,
вырезать, сложить, проверить зазоры — час работы на конструкцию.
- **AST-парсер выражений** взамен `new Function()`. До многопользовательской
БД нельзя дальше с `eval`-эквивалентом. Реалистично — `expr-eval` (10KB
готовая либа) или ~300 строк своего PEG-парсера. См. ROADMAP уровень 1.
- **Bleed offset** если печать на коробках планируется (вопрос открыт).
- **Punch holes / окна** если нужны для подарочных конструкций.
- **Метки регистрации** — обязательны для офсетной/флексо-печати, тривиально.
- **Валидация геометрии** перед production: замкнутость контура,
самопересечения, влезание на лист, толщина в зазорах. См. ROADMAP уровень 1.
- **Выбор итоговых 5-15 конструкций** — приоритет по реальным заказам Sunprint
(рекомендуется ревью истории в SunTask на этапе планирования).
Артефакт Фазы 1: устойчивая локальная версия + JSON-каталог, который можно
без переделок мигрировать в БД print-calc.
### Фаза 2 — интеграция в print-calc (3-5 сессий)
Когда Фаза 1 даёт устойчивый каталог — переносим в Sunprint-стек:
- **Prisma миграция:** новая таблица `BoxConstruction` (или JSON-колонка
`geometryDef` в существующей `Template` — решение по «Открытому вопросу 3»).
- **Pipeline-handler:** новый case `box_construction` в `prepareEngineInput.ts`.
По аналогии с `card_deck`: UI шлёт `templateSlug + parameters`, pipeline
резолвит шаблон через `getTemplateBySlug()`, считает геометрию через
портированный TS-evaluator, превращает её в engine-input для расчёта цены.
- **Engine остаётся семантически тупым** (Урок 2). Цена считается от
`tirage` (количество коробок) + площади развёртки + материала. Геометрия
уходит в `_meta` для UI и для DXF.
- **UI-форма:** в `Calculator.tsx`/`ManagerCalculator.tsx` при
`extraFields._type === 'box_construction'` — рисуем поля параметров (L/W/D/t
и т.п.), live-превью SVG, кнопка «Скачать DXF».
- **DXF на скачивание:** генерируется на бэке (Node `dxf-export.ts`,
портированный из `dxf-export.js`). Клиент получает либо ссылку, либо
файл-аттач к КП. Решение по «Открытому вопросу 4».
- **Тесты:** prepareEngineInput тесты по образцу card_deck (8-12 кейсов),
отдельно тесты geometry-evaluator (10-15 кейсов с golden DXF/SVG).
- **Обязательные коды ошибок** (Урок 4): `BOX_PARAMS_REQUIRED`,
`BOX_TEMPLATE_NOT_FOUND`, `BOX_GEOMETRY_INVALID`, `UNHANDLED_TEMPLATE_TYPE`.
### Открытые решения для Фазы 2 (TBD)
| # | Решение | Влияние | Когда решать |
|---|---|---|---|
| 1 | Топ-5/10/15 конструкций под Sunprint | объём Фазы 1 | до старта Фазы 1, требует ревью SunTask-заказов |
| 2 | Модель цены: площадь / FEFCO-прайс / раскладка на лист | сложность handler'а | до старта Фазы 2 |
| 3 | Каталог: JSON в репо / `BoxConstruction` Prisma / JSON-колонка в `Template` | миграция + UI правок | до старта Фазы 2 |
| 4 | DXF: ссылка / аттач к КП / отправка в производство напрямую | API + UI правок | до старта Фазы 2 |
| 5 | Bleed offset нужен? | объём Фазы 1 (~200 строк или 0) | зависит от: «есть ли печать на коробках» |
| 6 | Кто работает с конструктором в проде: менеджер / клиент / оба | UI и Auth-уровень | Фаза 2 |
| 7 | Перенос evaluator на TS — переписывать или транспилировать JS как есть | трудозатраты Фазы 2 | до старта Фазы 2 |
### Граница: что Sunprint НЕ делает с этим конструктором
Чтобы не превратить инструмент в полный CAD (см. ROADMAP § «Что НЕ делать»):
- Не реплицируем ArtiosCAD/Esko Studio. Это software за десятки тысяч
долларов и десятилетия разработки.
- Не делаем визуальный редактор контура мышкой — JSON + параметры остаются
основным интерфейсом для дизайнера/админа.
- Не покрываем все 200 кодов FEFCO. Только то, что реально заказывают.
- Не делаем 3D-превью складывания на старте. Может появиться позже.
---
## ✅ Закрытые треки
### 2026-04-28 (15:21): Basic Auth снят с calc.suntask.ru — `/api/v1/calculate` open
**Цель:** убрать MVP-защиту Basic Auth (manager:<htpass>), которая мешала AJAX-вызовам из embed.
**Симптом** — после деплоя SSO bridge: при нажатии «Рассчитать» в калькуляторе из `/embed?token=...` появлялся нативный browser-диалог Basic Auth. JWT уже принят, юзер залогинен, но nginx перед proxy_pass требовал `auth_basic`. POST `/api/v1/calculate` всегда отдавал **401 574** до Next.js.
**Корень** в `/etc/nginx/vhosts/njsoft/calc.suntask.ru.conf`:
```nginx
server { # server-block корень
auth_basic "Print Calculator"; # ← применялось ко всему vhost'у
auth_basic_user_file /var/www/print-calc/.htpasswd;
location /api/auth/ { auth_basic off; ... } # явные исключения
location /login { auth_basic off; ... }
location /manager { auth_basic off; ... }
location /api/v1/quotes { auth_basic off; ... }
location /api/v1/clients { auth_basic off; ... }
location /embed { auth_basic off; ... }
# ...
location /api/ { ... } # ← БЕЗ auth_basic off!
# /api/v1/calculate, /api/v1/catalogs,
# /api/v1/templates сюда попадают
}
```
**Правка:** удалены 2 строки server-block через `sed -i`:
```diff
- auth_basic "Print Calculator";
- auth_basic_user_file /var/www/print-calc/.htpasswd;
```
Все 9 `auth_basic off;` в локациях оставлены (idempotent — no-op без главного `auth_basic`). Можно убрать в plain-text-чистке потом, не критично.
**Smoke 28.04 16:27:**
```
<manager-1-ip> GET /calc-redirect.php → 302
<manager-1-ip> GET /embed?token=<новый JWT> → 200 (1.8 KB)
<manager-1-ip> POST /api/v1/calculate → 200 (910 байт JSON)
<manager-2-ip> (другой manager) POST /api/v1/calculate → 200, 200 (15:57)
```
HTTP-distribution на `/api/v1/calculate`: было 4×401, стало **4×200** (Дима + другой менеджер). Никаких регрессий.
**Откат:** `cp /etc/nginx/vhosts/njsoft/calc.suntask.ru.conf.bak-pre-rm-basicauth-20260428-152138 → vhost && nginx -s reload`
### 2026-04-28: SunTask SSO Bridge — задеплоен и работает в проде
**Цель:** менеджер SunTask кликает «Калькулятор» в admin-меню → автоматически попадает в `calc.suntask.ru/embed` с правильной ролью без отдельного логина.
**Финальная архитектура (живая):**
```
suntask.ru/calc-redirect.php (standalone PHP вне ZF1)
↓ session_save_path('/var/www/html/sessions') + session_start()
↓ $email = $_SESSION['Zend_Auth']['storage']
↓ PDO connect через njsoft_sun_adv.db.host (creds из Lv7CMS/config.ini [production])
↓ SELECT id, email, role, real_name FROM _users WHERE email=? AND site='suntask' AND activity=1 AND deleted=0
↓ mapRole: administrator/developer → owner | staff → manager | else → 403
↓ JWT::encode HS256 (claims: email, name, role, iat, exp=300, iss=suntask.ru, sub=u<id>)
↓ 302 Location: https://calc.suntask.ru/embed?token=...&next=/manager
calc.suntask.ru
↓ JWT verify (тот же secret из application.ini production calc.jwt_secret)
↓ /embed page render → 200
↓ /api/v1/clients?limit=10 → 200 (live data)
менеджер работает в калькуляторе ✅
```
**Smoke на live (28.04 14:48 MSK):**
```
suntask: GET /calc-redirect.php → 302 (без ошибок в php-fpm.log/error.log)
calc: GET /embed?token=<JWT>&next=/manager → 200
calc: GET /_next/static/chunks/app/embed/page-*.js → 200
calc: GET /api/v1/clients?limit=10 → 200, 200, 200, 200 (Дима реально печатает в поиске)
```
JWT-payload (декодированный, при браузерном тесте Димы):
```json
{"email":"<owner email — в защищённом хранилище>",
"name":"<реальное имя owner-а>",
"role":"owner",
"iat":1777376890, "exp":1777377190, "iss":"suntask.ru", "sub":"u190"}
```
**Архитектурный путь** (3 версии файла + reverse-engineering Lv7CMS):
- v1 standalone с `Zend_Application::bootstrap()` → 302 на /admin для всех (Lv7CMS user-registry не заполняется без dispatch)
- v2/v3 CalcController внутри taskManager-модуля → 404 (Lv7CMS routing engine читает routes из БД, UI menu-add не регистрирует route)
- v4 standalone PHP без bootstrap → PDO `[2002] No such file or directory` (host=localhost из dev-секции config.ini)
- v5 + `session_save_path()` → дотянулся до session, но PDO та же ошибка
- **v6** + `parse_ini_file(true)['production']` → ✅ работает
Все 5 архитектурных уроков см. ниже в «📚 Уроки проекта».
### 2026-04-27: БАГ колод (тираж ×54) — v8.1 в проде
**Корень:** `extraFields.cardsPerDeck` хранился только в seed JSON, ни одна часть кода (engine, API, UI) его не читала. UI-загрузчик шаблона брал `defaultInput.tirage = 100` как тираж карт, менеджер интерпретировал как 100 колод. Engine считал 100 карт вместо 5400 → сметы в 54× дешевле.
**Решение:** pipeline между UI и engine (`apps/web/lib/calc/prepareEngineInput.ts`) с handler-registry. UI шлёт `templateSlug + decks`, pipeline переводит в `tirage = decks × cardsPerDeck`, engine не тронут. **30/30 engine-тестов остались зелёными**, +10 новых pipeline-тестов = **93/93**.
Smoke на live: 101 колода × 54 = 5454 карт ✅, productsPerSheet=21 (golden Excel match), авто-shrinkWrap 50₽×101=5050₽.
Контракт ошибок: TEMPLATE_NOT_FOUND/DECKS_REQUIRED → 400, UNHANDLED_TEMPLATE_TYPE → 500.
Бэкап `/var/www/print-calc.old.20260427-pre-v81` снят — 24-часовое observation прошло без регрессий (28.04 утром: 0 ошибок в logs за сутки, HTTP 4×200/1×400/0×500).
---
## 📋 Бэклог (по приоритету)
**Легенда приоритетов:**
- 🔴 **Критично** — кровотечение прямо сейчас (баги в проде, потеря денег, потеря данных). Бросаем всё и фиксим.
- 🟡 **Важно** — функционал ожидается заинтересованным лицом, без него работа неполная. Делается в плановом порядке.
- 🟢 **Полезно** — улучшения, которые не блокируют ничего. По возможности.
- 🔵 **Гигиена** — мелкие операционные задачи (смена паролей, очистка снэпшотов, обновление зависимостей).
| Приоритет | Трек | Статус | Оценка | Заметки |
|---|---|---|---|---|
| 🟡 | **Конструктор коробок (dieline) — Фаза 1: локальный плейграунд** | план зафиксирован, ждёт observation+пароли | ~1-2 недели локально | demo.html на машине оператора. Расширение каталога под микрогофру (топ 5-15), AST-парсер вместо new Function(), валидация геометрии. См. полный раздел «🆕 Трек: Конструктор коробок» выше |
| 🟡 | **Конструктор коробок — Фаза 2: интеграция в print-calc** | ждёт окончания Фазы 1 | ~3-5 сессий | Prisma `BoxConstruction`, handler `box_construction` в prepareEngineInput, UI-форма с DXF, цена. См. «Открытые решения для Фазы 2» выше — 7 пунктов TBD |
| 🟢 | calc-сторона Phase 4: NextAuth session из JWT | переоценить | 1-2 дня | **не блокер**: после снятия Basic Auth (28.04) `/api/v1/calculate` отдаёт 200 без NextAuth session. Phase 4 имеет смысл только если позже захотим разделить read/write права на calc-API (например аналитика для owner vs обычный расчёт). Понижено с 🟡 до 🟢 |
| 🟡 | print-calc /admin/approvals UI (owner) | не начат | 3 часа | владелец подтверждает скидки/наценки. ApprovalRequest пишется в БД с v7 |
| 🟢 | print-calc v8.2 rebase под v8.1 pipeline | не начат | пересмотреть | архив `print-calculator-v8-code.zip` устарел: содержит НДС 22%/курсы ЦБ/налог.окружение/шелкография/Equipment, но без pipeline-фикса колод. Нужен ремердж перед деплоем |
| 🟢 | Accountant CMS (редактор каталогов) | не начат | 2-3 дня | бухгалтер правит цены материалов/процессов |
| 🟢 | LIVE-CALC расчёты для тюнинга engine | не начат | по мере проблем | сравнение с реальными сметами |
| 🟢 | **njsoft_sun_adv DB password — accepted risk** | принято решение оператора | n/a | DB password засветился в claude.ai-чате 28.04 при `docker inspect`. **Не ротируется**: njsoft — external team с административным доступом к серверу, бэкап-канал для критических инцидентов, ротация порвёт этот канал в обмен на низкоинтенсивную гипотетическую защиту от утечки через Anthropic-инфраструктуру. См. правило 7 Security-политики |
| 🔵 | Сменить пароли manager + owner от 27.04 | shared server | TBD | висят в `/root/print-calc-initial.txt` (засветились 27.04 при `cat`). Ротация требует координации с другими пользователями сервера |
| 🟢 | password-source mystery в SunTask SSO bridge | не блокер | TBD | `application.ini` SunTask **не содержит** `db.default.params.password` (grep'ом 0 совпадений), но `calc-redirect.php` через `parse_ini_file('production')` живёт и SSO работает. Расследовать откуда читается DB password — для документирования архитектуры (расширение Урока #9). Кандидаты: `Lv7CMS/config.ini`, `resources.db.params.password` (другой Zend key), env-переменная контейнера |
| 🔵 | Поднять swap 4-8 GB на сервере | не начат | 5-10 мин | сейчас swap=0, PSI чистый — но без swap-парашюта при OOM-инциденте процесс падает мгновенно. Страховка для крупных билдов / неожиданных пиков. Замечено при diagnostic-разведке 29.04 |
| 🟢 | Очистка диска / расширение `/dev/md43` | в течение квартала | TBD | 79% занято (652G/878G, 181G свободно). Сейчас не блокер, но если рост контейнерных volumes продолжится — упрётся. Замечено при diagnostic 29.04 |
| 🟢 | Расследование `njsoft_sun_adv_db` 86% CPU spike | не срочно | TBD | snapshot 29.04 10:05 показал 86% CPU + 4.27 GB RAM на этом контейнере, load avg 1.20 (norm). Точечный пик, не критично. Если повторится в графике долгой нагрузки — log-analysis MySQL slow-query |
| 🔵 | Вычистить `.bak`-конфиги в `/etc/nginx/vhosts/njsoft/` | не срочно | 5 мин | два файла регистрируют `server_name suntask.ru``nginx -t`/reload каждый раз ругается warning'ами `conflicting server name`. Кандидаты: `calc.suntask.ru.conf.bak-ip-fix`, `calc.suntask.ru.conf.bak-pre-rm-basicauth-20260428-152138`. Перенести в `/root/nginx-bak/` или удалить — функционально не нужны |
---
## 🚧 Известные риски / грабли
- **claude.ai-сессии могут работать с устаревшим Project Knowledge.** 29.04 другая claude.ai-сессия обновила STATE.md от 27.04-baseline (две сессии назад), затёрла контент 28.04 вечера: трек «Basic Auth снят», трек «SunTask SSO Bridge», все 5 уроков Lv7CMS reverse + урок про nginx Basic Auth (6 уроков), упоминание `bak-pre-rm-basicauth-20260428-152138`. Поймали через md5-сверку в Code, восстановили merge'ем из архива. **Правило:** перед обновлением STATE.md в claude.ai сравнивать md5 локального Project Knowledge с актуальной версией в репо/на сервере. Если расходятся — сначала pull/refresh, потом правки. Архив на X-drive (`/x/-=SOFT=-/Claude/dima/2026-04-29_*_STATE_*.md`) — последний рубеж восстановления.
- **v8.2 архив устарел.** Pipeline-фикс делался поверх v7. v8 (НДС 22%, курсы ЦБ) собирался в claude.ai-чате до этого. Перед деплоем v8.2 — нужно смержить v8 правки с v8.1 (новые `lib/calc/`, `lib/templates.ts`, расширение Zod в route.ts).
- **CalcController.php в taskManager — мёртвый груз.** v3 deployed, но Lv7CMS routing его не использует. Можно оставить (reference для будущих случаев) или удалить. На усмотрение оператора. Не вредит.
- **Контекст между средами теряется** → правило: STATE.md обновляется в конце каждой сессии. Иначе следующая сессия теряет находки.
---
## 📦 Артефакты последней сессии
### SunTask SSO bridge — deployed (28.04.2026)
| Файл | md5 | Статус |
|---|---|---|
| `/var/www/sun-promo.docker/src/library/PhpJwt/JWT.php` | `cb678129885e1d97e2f36bba95fe95c3` | active (HS256 encode/decode) |
| `/var/www/sun-promo.docker/src/modules/taskManager/controllers/CalcController.php` | `96081670f57d8dec6e7a7200516cd176` | **мёртвый груз** (v3 deployed, Lv7CMS routing не использует — оставлен как archaeology) |
| `/var/www/sun-promo.docker/src/www/suntask.ru/application/configs/application.ini` | mod | `[production]` секция расширена 3 строками `calc.jwt_secret` / `calc.jwt_issuer` / `calc.url` |
| `/var/www/sun-promo.docker/src/www/suntask.ru/www/calc-redirect.php` | `1a4c8739537d81f58d7a524dcdc78bdf` | **active (v6) — основной endpoint SSO** |
| Menu-item `/admin/menu` UI | DB | URL изменён на `/calc-redirect.php` (Дима, 28.04 ~11:49) |
### Bookmark backups (на сервере /tmp, держать до 7 дней стабильности)
```
/tmp/suntask-application.ini.bak-20260424-140650 — ini до Шага 3 (calc.* keys) [Шаг 0.5]
/tmp/suntask-application.ini.bak-20260428-095617-pre-step3 — ini до правки awk на Шаге 3.3
/tmp/suntask-menu.sql.bak-20260424-140650 — mysqldump _sitestructure_node до Шага 0.5
/tmp/calc-redirect.php.bak-pre-v4-20260428-132849 — v1 standalone (с Zend_Application bootstrap)
/tmp/calc-redirect.php.bak-pre-v5-20260428-142041 — v4 (без bootstrap, до session_save_path)
/tmp/calc-redirect.php.bak-pre-v5-20260428-142315 — v4 дубль
/tmp/calc-redirect.php.bak-pre-v6-20260428-143950 — v5 (до parse_ini_file fix)
/tmp/CalcController.php.bak-pre-v3-20260428-105448 — CalcController v2 (до Lv7CMS_Controller_Backend)
/etc/nginx/vhosts/njsoft/calc.suntask.ru.conf.bak-pre-rm-basicauth-20260428-152138 — calc nginx vhost ДО снятия auth_basic
```
---
## 🔗 Связанные документы и среды
| Документ | Где | Содержит |
|---|---|---|
| `project_print_calc.md` | Claude Code memory | пути, контейнеры, env, deploy-команды |
| `project_suntask_sso_deploy_active.md` | Claude Code memory | подшаги 0-7 SSO деплоя (теперь historic) |
| `feedback_state_md_single_source.md` | Claude Code memory | регламент работы с этим файлом |
| `DEPLOY.md` | репо print-calc | план развёртывания calc.suntask.ru |
| `SKILL.md` (web-master) | `/mnt/skills/user/web-master/` | стандарты SEO/UX/UI |
| `SEO__и_сайт.rtf` | Project Knowledge | стандарты разделов sunprint.ru, NAP, копирайтинг |
| Excel-файлы (3 шт.) | Project Knowledge | исходная логика расчёта (для квиза-калькулятора) |
| `README.md`, `ARCHITECTURE.md`, `SCHEMA.md`, `ROADMAP.md`, `INTEGRATION.md`, `DECISIONS.md` (dieline) | Project Knowledge | handoff-пакет конструктора коробок v0.4 |
| `catalog.json`, `core.js`, `dxf-export.js`, `demo.html` (dieline) | Project Knowledge | данные + рабочий код v0.4, vanilla JS |
---
## 📐 Правила работы со STATE.md
1. **Один файл — одна правда.** Не плодить дубли.
2. **Обновлять в конце сессии.** Любой Claude — последним действием. Если по треку ничего не изменилось, ставится новая дата и «без изменений».
3. **Жёсткая структура.** Заголовки выше — фиксированные. Не переименовывать, не перемешивать. Это позволяет diff'ать между сессиями.
4. **Читать в начале сессии.** Любой Claude — первым делом. Если в контексте нет — спросить пользователя, где взять.
5. **Хранить в git.** Чтобы история изменений была. `git log STATE.md` — таймлайн всего проекта.
6. **Сюда НЕ кладём:** пароли, токены, API-ключи, личные данные клиентов, суммы конкретных сделок. Только техническая правда о ходе работ.
7. **Сюда кладём:** активный трек, что выяснено, гипотезы, следующий шаг, бэклог, риски, ссылки.
8. **Перед обновлением — md5-сверка.** Если работаешь в claude.ai через Project Knowledge, сравни md5 загруженной копии с актуальной (репо/сервер). При расхождении — сначала refresh, потом правки. Иначе риск регрессии (см. инцидент 29.04 в рисках).
9. **Public mirror работает только с redacted-копией.** Полный STATE.md (с реальными IP менеджеров, путями инструментов оператора, internal-деталями) живёт в **приватном** репо `git.suntask.ru/sunprint/print-calc` (или X-локально + сервер). Публичный mirror `git.suntask.ru/sunprint/sunprint-state` получает **redacted-версию**: IP-адреса менеджеров заменены на плейсхолдеры (`<manager-N-ip>`), сервер `212.41.28.51` остаётся (DNS-публичный, не секрет). При обновлении полного — повторно прогнать sed-redact на копии, проверить через `grep -E '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b'`, что остался только `212.41.28.51`, только потом коммит/push в public.
**Версионирование URL для claude.ai (обязательно):** claude.ai web_fetch держит собственный кеш на стороне Anthropic-инфраструктуры, **не реагирующий на downstream Cache-Control headers** (даже `no-cache, must-revalidate` — проверено 29.04). Поэтому branch-URL `/raw/branch/main/STATE.md` для claude.ai **НЕ работает** — отдаёт устаревшую версию (та, что claude.ai увидел при первом fetch'е).
Стандарт: для каждого STATE.md update Code в отчёте указывает short-hash последнего commit'а **и готовый URL** формата `https://git.suntask.ru/sunprint/sunprint-state/raw/commit/<short-hash>/STATE.md`. claude.ai делает web_fetch на этот URL — он уникален для каждого commit'а, кеша никогда не было, всегда отдаёт нужную версию.
nginx `Cache-Control: no-cache, must-revalidate` (с `X-Source: raw-passthrough` маркером) для path'ов `^/[^/]+/[^/]+/(raw|media)/` в `/etc/nginx/vhosts/njsoft/git.suntask.ru.conf` остаётся как страховка для остальных HTTP-клиентов (`curl`, браузеры, второй разработчик, будущие интеграции). Не вредит.
---
## 🔒 Security политика для claude.ai/Code сессий
Три инцидента с утечкой credentials в чаты за две недели: 27.04 (manager+owner пароли через `cat /root/print-calc-initial.txt`), 28.04 (DB password через `docker inspect`), 29.04 (Gitea token через clone-URL → `cat .git/config`). Этот раздел — правила, выработанные на этих инцидентах.
1. **Любой credentials, попавший в текстовый output одной из сессий (claude.ai или Code), считается скомпрометированным с момента отправки.** Без оценки вероятности утилизации, без «там же только минута». Ротируется немедленно. Никогда не делаем исключения вида «доделаем последнюю команду и потом revoke».
2. **Не использовать `cat` / `docker inspect` / `env` / `grep` на файлах с секретами.** Использовать точечный `awk` / `sed` по нужному ключу, передавать наружу **только результат**, не raw содержимое. Для проверок чистоты — бинарный exit code (`grep -q && echo OK || echo DIRTY`), не stdout значения.
3. **Token в URL ≠ inline аргумент.** `git clone https://user:token@...` записывает токен в `.git/config`. Использовать `GIT_ASKPASS`, `git credential approve`, env-var с inline `git -c credential.helper`, или SSH-ключи.
4. **Доставка токенов на сервер — через pipe, без промежуточного файла:**
```bash
echo -n 'TOKEN' | ssh root@HOST 'umask 077; cat > /root/.token-name; chmod 600 /root/.token-name; wc -c /root/.token-name'
```
Пробел перед `echo` пропускает команду из shell history если `HISTCONTROL=ignorespace` (или эквивалент). Не использовать `nano`/`vim` для секретов, не делать локальные tempfile'ы.
5. **TTL на все токены, никогда no-expiration.** Стандарт: 90 days для рабочих токенов, 30 days для разовых операций. Истекший токен = автоматическая страховка от забытого `shred`.
6. **claude.ai-сессии ≠ secure transport.** Чат-история передаётся через Anthropic-инфраструктуру и хранится у них. Code-сессии — тоже. Любой текст, попавший в чат, считается прошедшим минимум через Anthropic-логи. Это не приватная переписка двух людей.
7. **Accepted risk — отдельная категория, не «забыли ротировать».** Когда credentials попали в чат, но ротация имеет операционную стоимость выше рисковой стоимости (legitimate parties с доступом, бэкап-каналы, привязка к внешним конфигам не нашим), фиксируется как **accepted risk** с явным обоснованием в бэклоге, не как 🔵 pending-задача. Это отличает «забыли» от «знаем и приняли осознанно». Применимо: `njsoft_sun_adv` DB password (29.04 — accepted risk: external team backup-channel).
---
## 📚 Уроки проекта (постоянные принципы)
Эта секция — не про текущий трек, а про архитектурные решения, которые сложились в ходе работы над print-calc и должны соблюдаться следующими сессиями. Если приходит идея «давайте перенесём логику X в UI — так быстрее», сначала загляни сюда: возможно, мы уже это решили иначе и по веским причинам.
### print-calc — pipeline и engine
#### 1. Композитные продукты не живут в UI
Доменная семантика «1 колода = N карт» — в `apps/web/lib/calc/prepareEngineInput.ts`, не в `Calculator.tsx`/`ManagerCalculator.tsx`. UI спрашивает и показывает, сервисный слой решает «как это превратить в engine-input».
Когда появится следующий составной продукт (наборы открыток 12-в-1, многосекционные альбомы, альбомы стикеров) — добавляется новый `case` в `handlers` registry, **без правок UI и engine**. UI просто читает `extraFields._type` шаблона и рисует подходящие поля ввода; pipeline на сервере преобразует.
#### 2. Engine семантически туп
`packages/engine` принимает `tirage` как количество штук на резке, считает раскладку. Не знает про «колоды», «наборы», «секции». Это позволило не переписывать 30 зелёных engine-тестов при v8.1: контракт `QuoteInput.tirage` не изменился, добавился только сервисный слой над ним.
Любой новый смысл (составные единицы, многоэтажные расчёты, скидки дилеров) — кандидат **сначала в pipeline**, и только если паттерн повторится 2-3 раза — миграция в engine как нативная абстракция (см. план v9 по `productGrouping`).
#### 3. Unknown `_type` → 500, не silent passthrough
Если в `seed.json` появится шаблон с новым `extraFields._type`, а handler в `prepareEngineInput.ts` не зарегистрирован — pipeline падает с `UNHANDLED_TEMPLATE_TYPE` HTTP 500 + `console.warn` в логи. **Никакого тихого пропуска.**
Защищает от классики: добавили в БД новый тип через accountant CMS, забыли написать handler, расчёт молча идёт по неверной ветке (или сваливается в passthrough как обычный продукт), баг живёт месяцами и проявляется как «странные цифры в КП». Лучше явная 500 в первый же день.
#### 4. Явные коды ошибок на уровне pipeline
`TEMPLATE_NOT_FOUND` (400), `DECKS_REQUIRED` (400), `UNHANDLED_TEMPLATE_TYPE` (500) — клиент сразу понимает что чинить: поправить slug, добавить поле, написать handler. Не «something went wrong» и не engine-ошибки наружу.
При добавлении новых типов и handler'ов — следовать тому же паттерну: домен-специфичные коды (`SET_SIZE_REQUIRED`, `SECTIONS_REQUIRED` и т.п.), не обобщённые `INVALID_INPUT`.
#### 5. Recon перед фиксом окупается
Перед v8.1-фиксом вместо «сразу делаем legacy-режим на всякий случай» прошлись логами nginx, app-логами, БД, кодом suntask: 10 минут команд → обнаружено, что внешних API-клиентов **нет** → отказались от legacy-режима и сэкономили ~1 час кода + 1 тест-кейс. См. строку «БЛОК 1.1 — 263 запроса, все из 2 UI» в архиве чата.
Принцип: перед добавлением «защитного» кода (deprecation, fallback, dual mode) — 5-15 минут на проверку, нужна ли защита вообще. Часто оказывается, что нет.
### Lv7CMS reverse-engineering (28.04.2026)
Архитектурные сюрпризы Lv7CMS, которые мы проходили через 6 итераций при SSO-интеграции. Сохраняем чтобы следующие интеграции (или другие SunTask-задачи) не наступили на те же грабли.
#### 6. Lv7CMS routing — из БД, не из файла
Все маршруты SunTask регистрируются динамически в `Lv7CMS::init()` через `Zend_Registry::get('Routes')->findAll()``Zend_Controller_Front::addRoute()`. Источник — таблица в БД, читаемая через `Lv7CMS_Resource_Item_Route`.
UI «Добавить пункт меню» в `/admin/menu` создаёт **только меню-запись** (`_users_menu`), но **не route**. Если нужен новый URL вида `/admin/<module>/<controller>` для нового контроллера — нужен либо INSERT в `_routes` через UI/SQL, либо обходной путь.
**Standalone PHP в DocumentRoot** (`*.php` файл вне ZF1 routing) — самый простой обход: nginx → php-fpm напрямую, без Zend_Controller_Front. Так сделан `calc-redirect.php`.
#### 7. `Zend_Application::bootstrap()` без `->run()` ≠ полный bootstrap
`bootstrap()` инициализирует resource'ы (db, cache, view, FrontController), но **НЕ запускает dispatch loop**. Lv7CMS заполняет `Zend_Registry::set('user', ...)` через FrontController plugin (`Users_Plugin_Auth::routeStartup`), который активируется **только при `dispatch()` или `run()`**.
Standalone-скрипту, которому нужен текущий юзер: **читать `$_SESSION['Zend_Auth']['storage']` напрямую** (это email, identity hint), затем делать собственный PDO к `_users` и mapping role. Не ходить через `Zend_Registry::get('user')` — оно вернёт null.
#### 8. SunTask переопределяет `session.save_path` через application.ini
```ini
phpSettings.session.save_path = APPLICATION_PATH "/../../../sessions"
```
`/var/www/html/sessions/` (а не PHP-default `/tmp`).
Это применяется **только в ZF1 bootstrap** через `phpSettings.*`. Standalone-скрипты PHP **должны вручную** делать `session_save_path('/var/www/html/sessions')` ДО `session_start()`. Иначе session_start создаёт пустую сессию в `/tmp`, не находит существующего юзера, отправляет на login.
#### 9. `parse_ini_file($file, true, INI_SCANNER_RAW)` — process_sections=true ОБЯЗАТЕЛЕН
Lv7CMS/config.ini и SunTask application.ini имеют структуру:
```
[production]
db.default.params.host = "njsoft_sun_adv.db.host"
db.default.params.username = "sunprint"
[development : production] ← inheritance + override
db.default.params.host = "localhost"
db.default.params.username = "root"
```
`parse_ini_file($file, false)` (process_sections=false) читает плоско, последний ключ перезаписывает первый → **получим dev-значения вместо prod**. PDO connect ломается.
Правильно: `parse_ini_file($file, true, INI_SCANNER_RAW)['production']` — берёт **только production-секцию**, dev-override игнорирует.
#### 10. Docker env-переменные: видны под root, скрыты под app
`docker inspect <container> --format '{{range .Config.Env}}...'` показывает все env-переменные (включая `MYSQL_PASSWORD`) — но это работает **только под root**. Внутри контейнера под обычным пользователем (`app`, под которым работает php-fpm) `getenv('MYSQL_PASSWORD')` возвращает пустую строку.
Standalone PHP-скрипт **не может** надеяться на env-переменные для DB-credentials. Нужно **читать из конфиг-файла** (Lv7CMS/config.ini в нашем случае) с `parse_ini_file()`.
#### 11. MVP nginx-уровневый Basic Auth — снимать сразу как только полный auth-стек на месте
`auth_basic` в server-блоке nginx (вместе с `.htpasswd`) был MVP-защитой v7 — пока на calc-стороне не появился NextAuth + JWT-bridge. После того как полный auth-стек встал (NextAuth session для /manager, JWT для /embed) — Basic Auth превращается в **активный блокер** для всех AJAX-вызовов из подавторизованных страниц.
**Симптом:** при действии в подавторизованном UI всплывает нативный browser auth-dialog (поля username/password домена). Юзер видит «авторизуйся ещё раз» при том что только что прошёл JWT/SSO. Это почти всегда означает: nginx `auth_basic` срабатывает **до** proxy_pass на API endpoint, который не имеет `auth_basic off` исключения.
**Правило:** при добавлении любого временного nginx-уровневого auth — **сразу** добавить TODO-таск со сроком в STATE.md «снять при релизе X». Иначе он переживает все следующие auth-улучшения и мешает.
В нашем случае: 28.04 снят через `sed -i` 2 строк server-block + `nginx -s reload` (5 минут, без downtime, все `auth_basic off` локации стали idempotent).
### Архитектурные принципы за пределами card_deck
#### 12. Параметрическая геометрия живёт по тем же принципам, что калькулятор
Конструктор коробок (dieline) попадает в print-calc по той же модели, что `card_deck` (см. урок 1): данные в JSON/БД, чистые функции для расчёта, handler в `prepareEngineInput` — никакой геометрической логики в UI.
Конкретно:
- **Каталог конструкций — данные**, не код. Параметры (L/W/D/t), производные выражения, точки контура, сегменты резки/биговки — всё в JSON.
- **Evaluator чистый.** `buildGeometry(entry, params) → resolved geometry` без побочных эффектов. SVG/DXF-рендереры — отдельные чистые функции от результата evaluator'а.
- **`new Function()` нельзя оставлять в multi-tenant БД.** Если каталог редактируется не только владельцем — перед production-релизом обязательно заменить на AST-парсер выражений (`expr-eval` или PEG). Иначе любой сотрудник с правами правки шаблона может выполнить произвольный JS в браузере другого пользователя.
- **Граница ответственности.** Engine считает цену от tirage и площади. Геометрия — отдельный слой, который превращается в `_meta` для UI и в DXF на скачивание. По той же логике, что метаданные `units/itemsPerUnit` для card_deck.
Не пытаемся реплицировать ArtiosCAD. Цель — типовые конструкции с параметрами, для нишевого сегмента (микрогофра у Sunprint). Полный CAD — это отдельный мир и отдельная экономика.
### Security в claude.ai/Code сессиях
#### 13. Token в URL → токен в `.git/config`. Никогда `git clone https://user:token@...`
Git автоматически сохраняет URL аутентификации в `.git/config` нового клона под ключом `[remote "origin"] url`. Любая последующая команда, читающая `.git/config` (включая `cat`, `grep`, диагностические скрипты), выводит токен в свой output — а в claude.ai/Code это сразу попадает в чат-логи.
Безопасные альтернативы:
- `GIT_ASKPASS` обёртка с stdin
- `git credential approve` через pipe
- Inline `git -c credential.helper='!f() {...}; f'` (env-var живёт только время одной команды)
- SSH-ключи (когда настроим — сейчас приоритет в backlog)
Если **уже** клонировали с токеном в URL — **первой же командой**:
```bash
git remote set-url origin https://host/path.git # без креденшалов
grep -q 'token\|@host' .git/config && echo DIRTY || echo CLEAN
```
Никогда не делать `cat .git/config` или `grep .git/config` со stdout'ом значения — даже для проверки чистоты. Бинарный exit code через `grep -q ... && echo DIRTY || echo CLEAN` — единственный допустимый паттерн.
Инцидент 29.04: при smoke clone print-calc токен ушёл в чат через финальный `cat .git/config`. Поймали через 30 секунд после, отротировали. См. также раздел «🔒 Security политика для claude.ai/Code сессий» — этот урок №13 + 6 правил оттуда работают вместе.
---
## 📝 Лог сессий (последние 5)
| Дата | Среда | Длительность | Главное |
|---|---|---|---|
| 2026-04-30 | claude.ai (Opus 4.7) → Claude Code (Opus 4.7) | гигиена + push 0e53251 + правило #7 | (1) Push `0e53251` (правило #9 расширено — commit-pinned URL для claude.ai, ушло из локального pending в private remote). (2) Observation v8.1 формально закрыт: 0 ошибок за 48h, `2×200 + 1×400 + 0×500` на `/api/v1/calculate`, 0 quotes за 48h (менеджеры калькулировали без сохранения). (3) Snapshot `/var/www/print-calc.old.20260427-pre-v81` (956 KB) и `/var/www/print-calc/.htpasswd` (мёртвый артефакт) удалены. (4) `njsoft_sun_adv` DB password — accepted risk (правило #7 в Security политике): не ротируется, external team backup-channel. (5) Новый 🟢 backlog: «password-source mystery» — `application.ini` SunTask не содержит `db.default.params.password`, но SSO bridge живёт; расследовать откуда читается. Pre-flight memory↔Gitea API: чисто. |
| 2026-04-29 | claude.ai (Opus 4.7) → Claude Code (Opus 4.7) | планёрка + merge + Gitea-deployment + 2 incident'а + кеш-finding | (1) План dieline-трека: путь Б, Фаза 1 локально (1-2 нед), Фаза 2 интеграция (3-5 сессий). 7 решений TBD. (2) **Регрессия STATE.md:** claude.ai обновила файл от 27.04-baseline, затёрла 28.04. Поймали md5-сверкой, восстановили merge'ем. Урок №12 + правило #8. (3) **Gitea CE задеплоен на git.suntask.ru** — Let's Encrypt, nginx reverse-proxy на 127.0.0.1:3000, docker-compose в /opt/gitea, SQLite. Org `sunprint`, public `sunprint-state` (правило #9, redacted), private `print-calc`. (4) **Incident: token в URL** при smoke clone попал в чат через `cat .git/config`. Поймали за 30 сек, отротировали. Урок №13 + Security-секция. Новые backlog: swap, disk, db-spike, .bak-конфиги. (5) **Finding: claude.ai web_fetch имеет агрессивный собственный кеш на стороне Anthropic-инфраструктуры**, не уважающий downstream `Cache-Control` (даже `no-cache, must-revalidate`). nginx-override применён (для других клиентов остаётся защитой), но для claude.ai используется commit-pinned URL `/raw/commit/<hash>/STATE.md` как обязательный паттерн. Правило #9 расширено. |
| 2026-04-28 | Claude Code (Opus 4.7) | ~6ч | **Большой день. Полная end-to-end интеграция SunTask ↔ calc работает.** (1) SunTask SSO bridge: calc-redirect.php v1→v4→v5→v6, 6 итераций архитектуры. (2) Lv7CMS reverse-engineering: 5 уроков (routing из БД, bootstrap без dispatch, session_save_path override, parse_ini_file process_sections, env под app). (3) Basic Auth снят с calc.suntask.ru через `sed -i` + `nginx -s reload` — 6-й урок добавлен. Подтверждение: POST /api/v1/calculate → 200 от Димы (<manager-1-ip>) и от другого менеджера (<manager-2-ip>) — оба смогли реально использовать калькулятор |
| 2026-04-28 (утро) | Claude Code (Opus 4.7) | ~10мин | observation v8.1 прошёл — 24h без ошибок, 0 пятисоток, restic backup OK ночью |
| 2026-04-27 (вечер) | Claude Code (Opus 4.7) | ~3ч | **v8.1 БАГ колод задеплоен.** Pipeline `prepareEngineInput.ts` (вариант 1.5), 10 новых тестов, 93/93 зелёных, smoke на live прошёл |
| 2026-04-27 (утро) | claude.ai (Opus 4.7) | ~50мин | Корень бага колод: cardsPerDeck не пробрасывается в engine. Заведён STATE.md, договорились про pipeline-вариант |
> Старые сессии можно урезать до одной строки или удалять, оставляя последние 5-10. История целиком — в `git log`.