# QA: визуальный редактор AGIL Calls (`apps/editor`) — тотальный прогон

Дата: 2026-11-13. Стенды:

| Стенд | URL | Сборка |
|---|---|---|
| Прод (как у заказчика) | `https://calls.guardify.ru/editor/` | `index-CqYinDiS.js` (prod, до правок этого прогона) |
| Локальная симуляция прода после фиксов | `http://calls.test:8799/editor/` (Chrome `--host-resolver-rules`, реальный `@agil/calls-server` + demo dist same-origin) | текущий `apps/editor/dist` |

Инструмент: [`qa/editor/tools/editor-audit.mjs`](tools/editor-audit.mjs) (puppeteer-core, headless Chrome,
viewport 1440×1000, плюс 768×1024 и 390×844). Запуск:

```bash
# прод
EDITOR_API_KEY=<admin-key> node qa/editor/tools/editor-audit.mjs
# локальная симуляция прода
EDITOR_BASE=http://calls.test:8799/editor/ EDITOR_API_KEY=dev-key \
QA_HOST_RESOLVER="calls.test 127.0.0.1" QA_NO_PROXY=1 \
node qa/editor/tools/editor-audit.mjs
```

Скриншоты: [`screens/`](screens/) — **51 снимок** после фиксов (локальная prod-симуляция),
[`screens-prod/`](screens-prod/) — **51 снимок** базовой прод-сборки. JSON-отчёты:
[`artifacts/audit-result.json`](artifacts/audit-result.json),
[`artifacts/audit-result-prod.json`](artifacts/audit-result-prod.json).

## 1. Покрытие

**16/16 секций** пройдены по одной: Бренд, Тема, Фичи, Доступы, Лейаут, Аудио/видео,
Fallback, Безопасность, Сессия, AI-агент, Knowledge-агент, Интеграции, Провайдеры,
Масштабирование, In-call ассистент, Комната. На каждую секцию — снимок «как открылась»
и «после прохода по контролам» (32 снимка секций), плюс превью/экспорт/импорт/save/темы/
адаптив/фокус.

Автоматически прогнано (счётчики из `audit-result.json`):

| Тип контрола | Действий |
|---|---|
| `select` — пройдены **все** option с восстановлением значения | 148 |
| `switch` — двойной toggle (вкл/выкл) | 123 |
| текстовые/числовые поля — ввод + восстановление | 150 |
| color-picker — смена + восстановление | 37 |
| chips (фичи/коды/allowlists) — двойной toggle | 341 |
| кнопки/строки таблиц (add + remove строки) | 17 + 7 added / 7 removed |

Отдельные сценарии: пресеты темы (5), кнопка «Применить к теме», 36 цветовых токенов,
создание/редактирование custom role, матрица ролей, overrides, TURN/узлы/вебхуки/правила/
фоны (add/remove), 4 ссылки шапки, тема редактора light↔dark, Copy JSON / Download JSON /
Copy snippet, Import JSON (валидный, битый JSON, zod-ошибка), Save-to-tenant (успех и
неверный ключ), Copy embed, превью (ready, смена scheme, reload, новая вкладка), Tab-фокус
(30 переходов), 3 брейкпоинта.

## 2. Найденные проблемы и статус

Severity: P0 — блокер, P1 — ломает функцию, P2 — заметно мешает, P3 — косметика.

| # | Severity | Где | Проблема | Статус |
|---|---|---|---|---|
| F-01 | **P1** | превью | same-origin `/embed.html` без `room`/`tenant` → embed не монтировался, `agil:ready` не приходил, offline-overlay на проде | **Исправлено ранее и подтверждено на проде**: `src=…/embed.html?room=preview&tenant=demo&_r=…`, статус «preview подключён», overlay hidden, 0 ERR_CONNECTION_REFUSED |
| QA-01 | **P1** | превью (embed) | При промежуточно невалидном конфиге (добавили строку таблицы с пустым обязательным полем) редактор отправлял его в embed через `postMessage`; embed-обработчик парсил без try/catch → **uncaught ZodError** (на проде 4 шт: Доступы, Сессия, AI-агент, Интеграции) | **Исправлено** в `preview.ts`: `tenantConfigSchema.safeParse` перед отправкой; невалидный промежуточный конфиг не уходит в превью, пишется строка в лог превью. После фикса: `pageErrors = 0`. Рекомендация фронтенду (вне зоны редактора): ловить ZodError в embed-обработчике — defense in depth |
| QA-02 | **P1** | правая панель | Кнопка **«Копировать сниппет»** не имела обработчика — не копировала и не показывала тост (мёртвая кнопка) | **Исправлено**: клик копирует `snippetArea`, тост «Сниппет скопирован» / ошибка; проверено |
| QA-03 | **P2** | адаптив | На ≤1200px панель настроек уезжала **под превью** (order), на 390 шапка переносилась на 2–3 строки | **Исправлено CSS**: `pane-left order:1`, `pane-center order:2`, компактная шапка (flex-wrap, ссылки в одну строку). Mobile 390: сначала навигация/секции, затем превью, overflow 0 |
| QA-04 | **P2** | e2e | Не было `data-testid`/`data-test` — нестабильные автотесты | **Исправлено**: 39 `data-testid` (nav-секции, save-tenant, config-json, config-snippet, preview-iframe, import-json, save-status, header-ссылки, preview-stage/overlay/status) |
| QA-05 | **P3** | типографика | 36 подписей цветовых токенов/hex, индексы строк, effective-подписи и бейджи рендерились 9–10px | **Исправлено CSS**: 11px |
| QA-06 | P3 | производительность | CLS 0.079 (86 layout-shift, long tasks 1–2) на загрузке/смене секций — ниже порога 0.1, но заметны сдвиги | **Открыто** (P3): стабилизировать высоту превью/скелетон, чтобы CLS → <0.05 |
| QA-07 | P3 | консоль (только локальный HTTP-стенд) | 2 предупреждения «Cross-Origin-Opener-Policy ignored… untrustworthy origin» — артефакт http:// на тестовом хосте; на проде HTTPS их нет | **Не баг продукта** (P3, информационно) |
| QA-08 | — | негативный тест | 401 `PATCH /v1/tenants/demo/config` при проверке неверного API-ключа | Ожидаемо (тест error-пути) |

Итого после фиксов на локальной prod-симуляции: **bugs = 0** (`pageErrors = 0`,
failed requests = 0, overlaps = 0, horizontal overflow = 0 на 390/768/1440, focus
visible 30/30). Прод-базовая сборка (до этих правок) имела: 4 pageErrors ZodError,
51 снимок, мёртвую кнопку Copy embed, 9–10px текст, отсутствие data-testid.

## 3. Проверенные состояния (скриншоты)

- секции: `01…16-section-*.png` и `*-after-controls.png`;
- превью: `70-preview-ready`, `71-preview-scheme`, `72-preview-reloaded`, `73-after-new-tab`
  (iframe `…/embed.html?room=preview&tenant=demo&_r=…`, ready, overlay hidden,
  scheme light→dark пишется в `config.theme.scheme`, reload снова ready, новая вкладка —
  тот же embed URL);
- экспорт/импорт: `74-export-copy` (тосты «JSON скопирован»), `75-import-invalid-json`
  («Некорректный JSON: Unexpected end of JSON input»), `76-import-invalid-schema`
  («theme.scheme: Invalid enum value… 'neon'»), `77-import-valid`;
- save: `78-save-wrong-key` (error + подсказка), `79-save-success`
  («Сохранено · etag …»);
- embed: `80-embed-copy` («Сниппет скопирован», snippet содержит `AGILCalls.mount`);
- темы: `81-theme-dark`, `82-theme-dark-overview`;
- фокус: `83-keyboard-focus`;
- адаптив: `90-responsive-390-mobile`, `90-responsive-768-tablet`, `90-responsive-1440-desktop`,
  `91-responsive-390-access`.

## 4. Данные и безопасность прогона

- Save-to-tenant на проде выполнялся **без изменения контента**: конфиг тенанта `demo`
  предварительно выгружен (`artifacts/prod-config-before.json`), импортирован в редактор
  и сохранён; сравнение GET до/после — **0 отличий** (`prod-config-after.json`).
- API-ключ в отчёты/скриншоты не попадает (поле `password`, значение не логируется);
  в артефактах ключа нет.
- Негативные тесты: битый JSON, zod-ошибка, неверный ключ, несуществующий импорт — все
  показывают понятные сообщения и не ломают редактор.

## 5. Изменённые файлы (apps/editor)

| Файл | Что |
|---|---|
| `src/preview.ts` | guard `tenantConfigSchema.safeParse` перед `postMessage` (QA-01, F-01), `room`/`tenant` в URL, `data-testid="preview-iframe"` |
| `src/inspector.ts` | обработчик «Копировать сниппет» (QA-02), `data-testid` для JSON/сниппета/save/import |
| `src/main.ts` | `data-testid="nav-*"`, превью получает tenant, docs-ссылки от origin (F-07) |
| `src/styles.css` | мобильный порядок панелей и шапка (QA-03), 11px вместо 9–10px (QA-05) |
| `index.html` | `data-testid` на shell-элементах (QA-04) |
| `src/env.d.ts` | `VITE_PREVIEW_ROOM`, комментарии про дефолт preview URL |

Проверки: `npm run typecheck -w @agil/editor` ✅, `npm run build -w @agil/editor` ✅.

## 6. Рекомендации (открыто)

1. **P3 QA-06 (CLS):** зарезервировать высоту `preview-stage` и карточек до загрузки
   (skeleton/`aspect-ratio`), чтобы убрать 84–86 layout-shift.
2. **Кросс-зона (frontend):** в embed-обработчике `postMessage` оборачивать
   `resolveTenantConfig`/`updateConfig` в try/catch и не падать на невалидном патче —
   редактор уже не отправляет невалидное, но defense in depth нужен (QA-01).
3. **P3:** вынести «API docs/OpenAPI/API guide/API.md» в overflow-меню на <520px, когда
   появится i18n/доп. ссылки (сейчас помещаются в одну строку).
4. **P3:** добавить e2e-сценарии на базе `data-testid` в общий e2e (`npm run e2e`).

## 7. Итоговые счётчики

- секций пройдено: **16/16**;
- интерактивных действий: **148 select-проходов, 123 switch, 150 текстовых, 37 color,
  341 chip, 17 кнопок, 7 add/7 remove строк**;
- скриншотов: **51 после фиксов** + **51 baseline прод**;
- багов: найдено **6** (2×P1, 2×P2, 2×P3) + 1 ожидаемый негативный ответ + 1 артефакт
  локального HTTP-стенда; **исправлено 5**, открыто P3 (CLS) + рекомендация фронтенду.



---

## 8. UX-ретест по отчёту наивного QA (13.11.2026)

Стенд: локальная prod-симуляция (`@agil/calls-server` + текущий `apps/editor/dist` +
demo same-origin, host `calls.test:8799`), headless Chrome 1440×900 / 768×1024 / 390×844.
Артефакт измерений: [`artifacts/ux-retest.json`](artifacts/ux-retest.json).

### 8.1. «Сохранить в тенант» спрятано (P1 → исправлено)

- В правой колонке появился **sticky save dock** вверху (`[data-testid=save-dock]`):
  заголовок «Сохранить изменения», подпись `PATCH /v1/tenants/{tenantId}/config`,
  кнопка **«Сохранить в тенант»** и общий статус. Вторая кнопка — в блоке настроек
  сервера; обе вызывают один `performSave()` и синхронно показывают success/error.
- 1440: dock `y=87`, кнопка `y=105`, обе **inViewport=true**; после прокрутки правой
  панели до конца (`paneScrollTop=844`, JSON ~11k px) dock остаётся на `y=87`.
- 768/390: правая колонка больше не отдельный скролл — dock закреплён внизу viewport
  (`position: fixed`, до 560px ширины): 768 — `y=964` при высоте 1024; 390 — `y=770`
  при 844. Обе **inViewport=true**.
- Ссылка **«К настройкам»** (skip-link): раньше `top: -60` (за экраном). Теперь
  видимая пилюля слева внизу на desktop и над save dock на мобильных:
  1440 — `y=855` (h=900), 768 — `y=907` (h=1024), 390 — `y=727` (h=844);
  первый **Tab** фокусирует именно её (outline 2px, переход к `#fields`).
- Кнопок без доступного имени: **0**.

Скриншоты: [`93-save-dock-1440`](screens/93-save-dock-1440.png),
[`94-skip-link-focus`](screens/94-skip-link-focus.png),
[`94b-save-dock-scrolled`](screens/94b-save-dock-scrolled.png).

### 8.2. Светлая схема делала превью нечитаемым (P1 → исправлено в редакторе)

Причина: переключение схемы меняло только `theme.scheme`, а цветовые токены оставались
от прежней палитры. Добавлен `src/theme-scheme.ts` (тот же алгоритм, что
`themeForScheme` виджета): палитра пересобирается из пресета целевой схемы с сохранением
бренд-primary/accent, типографики, shape/spacing, логотипа и фона; нормализация идёт в
store (экспорт/PATCH) и перед `postMessage` в превью.

Проверено в iframe (same-origin, computed styles всех текстовых узлов):

| Схема | `theme.colors.background` / `text` в JSON | Текстов с контрастом < 4.5 |
|---|---|---|
| light | `#f3f6fb` / `#0f172a` | 0 |
| dark | `#080d16` / `#f1f5f9` | 0 |

Первичный прогон выявил единственное нарушение — **виджетный** элемент
`.agil-join-preview-hint` («Запрашиваем доступ к камере…»): в light-схеме
`#475569` на `#0f172a` = **2.36:1** (редактор этот CSS не контролирует). Оно передано
frontend-владельцу `@agil/calls-widget`; после его фикса (бейдж получил собственную
surface-подложку, placeholder — читаемый текст на подложке) и пересборки widget/demo
**ретест пройден**: нарушений <4.5:1 нет, худшие пары — 5.00:1 (light, сервер/шорткаты
на `#f3f6fb`), кнопка входа 5.17:1; подсказка камеры — **7.58:1** (light) и
**11.72:1** (dark), заголовок 16.48/17.76:1. Все 8 текстовых элементов join-screen
проходят AA.

Скриншоты: [`96-light-preview-1440`](screens/96-light-preview-1440.png),
[`97-dark-preview-1440`](screens/97-dark-preview-1440.png).

### 8.3. Мобильная шапка в 2–3 строки (P2 → исправлено)

- Шапка на ≤720px — **одна строка высотой 60px** на 390/768/1440 (раньше 2–3 ряда).
  Подзаголовок бренда, draft/tenant-pill скрыты; ссылки — в горизонтально прокручиваемой
  meta-полосе; переключатель темы вынесен из неё, всегда **34×34** и в viewport
  (1440: x=1388, 768: x=716, 390: x=346).
- Горизонтальный overflow страницы не появился.

Скриншоты: [`95-mobile-header-390`](screens/95-mobile-header-390.png),
[`95b-mobile-header-768`](screens/95b-mobile-header-768.png).

### 8.4. Изменённые файлы

| Файл | Что |
|---|---|
| `src/inspector.ts` | sticky save dock, общий `performSave()`/статус для двух кнопок, rename в «Сохранить в тенант» |
| `src/theme-scheme.ts` | новый: palette-correct `themeForScheme` для схем light/dark |
| `src/main.ts` | нормализация палитры при смене схемы (store → export/PATCH) |
| `src/preview.ts` | нормализация темы перед `postMessage` |
| `index.html` | skip-link `data-testid`, theme-toggle вынесен из meta-полосы |
| `src/styles.css` | skip-link всегда виден; save dock sticky/fixed; компактная однорядная шапка; кнопка темы не сжимается |


---

## 9. Маскот-помощник «Agil» (13.11.2026)

Стенд: локальная prod-симуляция (`@agil/calls-server` + `apps/editor/dist` + demo same-origin,
`calls.test:8799`), headless Chrome 1440×900 и 390×844, light/dark. Артефакт:
[`artifacts/mascot-verify.json`](artifacts/mascot-verify.json); скриншоты —
[`screens/mascot/`](screens/mascot/).

### 9.1. Что проверено

- **FAB «Аги»**: виден на всех 16 разделах, анимированный WebP
  `http://…/branding/mascot/animated/mascot-wave.webp` загружается (`imgLoaded=true`),
  размер 58px (44px на 390), safe-area, draggable с сохранением позиции.
- **Роль по местоположению**: 16/16 разделов совпали с ожидаемым маппингом
  (brand/theme/layout/features/media/fallback/ai/knowledge/assistant → config-guardian,
  access → roles-admin, security → security-reviewer, integrations/providers → api-helper,
  scaling → scaling-advisor, room/session → onboarding). В панели 7 ролей, стартеры
  меняются вместе с ролью; ручной выбор роли сохраняется до следующей смены раздела.
- **Чат**: отправка вопроса → сообщение пользователя + ответ ассистента; локальный режим
  (`POST /v1/admin/assistant` без DeepSeek-ключа вернул `fallback`) показывает бейдж
  «локальный режим» и тег «локально» на сообщении. История — localStorage.
- **Guard (critical change)**: `POST /v1/admin/guard/assess` вызывается перед PATCH.
  При `requiresConfirmation` открывается модалка с маскотом в состоянии **angry**,
  risk-бейджем, findings (title/description/impact/affected), рекомендациями и
  чекбоксом «Я понимаю последствия»; кнопка подтверждения disabled без чекбокса.
  - **Отмена**: статус «Сохранение отменено: критическое изменение не подтверждено.»,
    `PATCH`-запросов **0**.
  - **Подтверждение**: после чекбокса → «Всё равно сохранить» → ровно **1 PATCH**,
    статус «Сохранено · etag …».
  - **Локальный fallback**: при 404 guard-endpoint редактор строит консервативную
    локальную оценку; для конфига с `features.chat=false` показана модалка с бейджами
    «риск: high» + «проверка без сервера», finding и отмена без PATCH.
- **Брендинг**: `branding.mascotName` из импортированного конфига обновляет имя в панели
  (проверено «Аги QA»); `mascotEnabled=false` скрывает FAB/панель; `mascotExpressions.wave`
  переопределяет idle-картинку; битый `mascotUrl` через 2–6 с уходит в inline-fallback
  (инициалы).
- **Мобильный 390**: панель — full-screen overlay 390×844; FAB 44×44 в `(334, 672)`
  (bottom: 128px + safe-area) и **не пересекается** ни с save dock (`y=770`), ни с
  кнопкой «Сохранить в тенант» (`y=788`); при фокусе в input FAB скрывается
  (`is-input-hidden`).
- **Light/dark**: панель и guard корректно перекрашиваются (скриншоты 50/51).
- Ошибки страницы: 0 (только ожидаемые 404 от искусственно отключённых
  assistant/guard в тестах fallback и COOP-предупреждения локального HTTP-стенда).

### 9.2. Скриншоты

`01-brand.png` … `16-room.png` (16 разделов с FAB), `20-chat-open.png`,
`21-chat-reply.png` (локальный режим), `30-guard-modal.png`, `31-guard-cancelled.png`,
`32-guard-acknowledged.png`, `33-guard-confirmed-saved.png`,
`34-guard-local-fallback.png`, `40-mobile-panel.png`, `41-mobile-fab-hidden-input.png`,
`50-dark-panel.png`, `51-light-panel.png`, `52-branding-fallback.png`.

### 9.3. Файлы

| Файл | Назначение |
|---|---|
| `src/mascot.ts` | FAB/панель/чат, роли по разделам, branding, guard-модалка, локальный fallback, история |
| `src/mascot.css` | стили FAB/панели/guard, mobile full-screen, safe-area |
| `src/main.ts` | `createMascot` + `mascot.setSection()` при смене раздела, `mascot.sync(config)` |
| `src/inspector.ts` | `beforeSave` hook: guard → отмена/подтверждение → PATCH |
| `public/branding/mascot/**` | копия анимированных ассетов (fallback к origin `/branding/...`) |

---

## 10. Маскот v2: Agil, история чатов и память (13.11.2026)

Стенд: локальная prod-симуляция (`@agil/calls-server` + `apps/editor/dist` + demo
same-origin, `calls.test:8799`), headless Chrome 1440×900 и 390×844.
Артефакт: [`artifacts/mascot-v2-verify.json`](artifacts/mascot-v2-verify.json);
скриншоты: [`screens/mascot/v2-01-fab.png`](screens/mascot/v2-01-fab.png) …
[`v2-08-light.png`](screens/mascot/v2-08-light.png).

### 10.1. Результаты

| Проверка | Результат |
|---|---|
| Имя по умолчанию | `Agil` в заголовке панели, FAB/`aria-label` («Agil — помощник редактора») |
| Миграция старого конфига | импорт `branding.mascotName="Аги"` → в JSON и в панели **`Agil`** (пользовательские имена сохраняются) |
| FAB при открытой панели | `fabVisible=false`, класс `is-panel-open`; после **Escape** → `fabVisible=true`, панель закрыта |
| Компактность | роль/стартеры в `<details data-testid="mascot-options">`; после первого сообщения `optionsOpen=false`; `suggestionsHidden=true` |
| Чат | 2 сообщения (user+assistant), локальный fallback при отсутствии DeepSeek-ключа |
| `context.chatId` | присутствует в теле `POST /v1/admin/assistant` (`chat-mwf5…`) |
| `context.memory` | **170** символов ≤2000, содержит фрагмент другого чата; поиск по стемам («запись»/«записью») |
| История чатов | 2 сессии с авто-заголовками; поиск «запис» → 2; переименование → «QA чат»; удаление 2→1; после reload активная сессия восстановлена (2 сообщения, 1 чат) |
| Роли по разделам | 16/16 совпали с ожидаемым маппингом (см. `roles.allOk=true` в артефакте) |
| Guard v2 | модалка `requiresConfirmation` открывается, маскот `angry`; «Отмена» → «Сохранение отменено: критическое изменение не подтверждено.» (PATCH не выполняется) |
| Мобильный 390 | панель full-screen **390×844**, шапка **49px**, `fabDisplay=none` при открытой панели |
| Light/dark | `v2-07-dark.png`, `v2-08-light.png` |
| Ошибки страницы | 0 (без ожидаемых 404 fallback-тестов и COOP-предупреждений локального HTTP-стенда) |

### 10.2. Изменённые файлы

| Файл | Что |
|---|---|
| `src/mascot.ts` | v2: Agil + легаси-миграция, скрытие FAB при открытой панели/Escape, collapsible options, sessions localStorage, drawer (поиск/переименование/удаление/новый чат), `chatId`/`memory` в запросе, компактный layout |
| `src/state.ts` | `migrateLegacyMascotName()` при загрузке черновика, импорте и reset |
| `src/mascot.css` | FAB `is-panel-open`, options/details, history drawer, composer sticky, line-height, компактная мобильная шапка |
| `src/main.ts`, `src/inspector.ts` | контракт без изменений (`setSection`/`sync`/`beforeSave`) |


---

## 11. Превью с реальной комнатой и типографика (13.11.2026)

Стенд: локальная prod-симуляция (`@agil/calls-server` + `apps/editor/dist` + demo
same-origin, `calls.test:8799`), headless Chrome 1440×900 и 390×844.
Артефакты: [`artifacts/preview-join-verify.json`](artifacts/preview-join-verify.json),
[`artifacts/typography-verify.json`](artifacts/typography-verify.json).

### 11.1. Превью: реальная комната вместо `room=preview`

- При монтировании iframe редактор делает `POST /v1/rooms {tenantId, name:"Превью
  редактора"}` → `{room.id, token, expiresAt}` и грузит
  `/embed.html?room=<uuid>&token=…&tenant=demo`; iframe получает `agil:ready`,
  статус «preview подключён», в логе — `event: joined`; **`Room not found` — нет**.
- Токен только в URL same-origin iframe: в видимом тексте страницы его нет,
  в console — нет; «Новая вкладка» открывает URL **без токена**. Сессия кэшируется
  до смены tenant/server и обновляется за минуту до `expiresAt`/по «Обновить».
- Fallback при 403/404/сети: `?room=preview&tenant=demo&previewMode=1`; в iframe
  инжектится баннер «Превью темы… создайте комнату», кнопки **«Войти в звонок»** и
  **«Только слушать» disabled** (`data-agil-preview-join`), submit/клики перехвачены —
  ложного `Room not found` не возникает.
- Скриншоты: [`screens/preview-join-created.png`](screens/preview-join-created.png),
  [`screens/preview-join-previewmode.png`](screens/preview-join-previewmode.png),
  `preview-join-*-page.png`.

### 11.2. Типографика и контраст

- Введены `--agil-text-xs/sm/md/lg`, `--agil-space-1..6`, `--agil-line-height`,
  `--agil-min-font`; body — 13px/1.5.
- Все 9–11.5px подписи → **12px**; `--muted` затемнён, chips/бейджи/nav — на
  `--primary-strong`, `--danger` затемнён + `--danger-fg` для solid danger-кнопок.
- Headless-скан computed styles по **16/16 разделам**, панели маскота и guard-модалке,
  light/dark, 390/1440: текстов <12px — **0**, контрастных пар <4.5 — **0**
  (`typography.allOk=true`).
- Скриншоты: `typography-1440-light.png`, `typography-1440-dark.png`,
  `typography-390-light.png`, `typography-390-dark.png`,
  `typography-guard-light.png`, `typography-guard-dark.png`.
- Правила и токены: `docs/design-system.md`.

### 11.3. Изменённые файлы

| Файл | Что |
|---|---|
| `src/preview.ts` | создание одноразовой комнаты, кэш/TTL, token только в iframe URL, previewMode fallback + guard в iframe |
| `src/main.ts` | `getServerUrl` для `POST /v1/rooms` (same-origin на проде) |
| `src/styles.css` | design-токены, 12px минимум, line-height 1.5, контраст chips/badges/danger, `--danger-fg` |
| `src/mascot.css` | 12px минимум для бейджей/подсказок, `--danger-fg` |
| `docs/design-system.md` | новый документ: шрифты/цвета/отступы/иконки/контраст + список правок |
| `docs/editor.md` | раздел превью с комнатой + раздел дизайн-токенов |


---

## 12. Тест-сценарий в превью: 1–50 синтетических участников (13.11.2026)

Стенд: локальная prod-симуляция (`@agil/calls-server` + `apps/editor/dist` + demo с
поддержкой `agil:simulate-participants`, `calls.test:8799`), headless Chrome
1440×900 и 390×844. Артефакт:
[`artifacts/simulation-verify.json`](artifacts/simulation-verify.json); скриншоты —
[`screens/mascot/sim-*.png`](screens/mascot/).

### 12.1. UI и контракт

- Панель **«Тест-сценарий»** в тулбаре превью: count 1–50 (number+range, дефолт 2),
  layout `grid/spotlight`, controls `bar/island/minimal`, «Применить»/«Сбросить»;
  localStorage per tenant `agil.editor.sim.v1.demo`.
- После `agil:ready` отправляется `{type:'agil:simulate-participants', count, layout,
  controls, requestId}`; ack `agil:simulate-participants-ack {ok:true,...}` →
  статус «подтверждено виджетом»; смена значений уходит без перезагрузки
  (debounce 250 мс).
- Caption под превью: «Тестовые участники: N (дизайн-превью, не реальные люди)»;
  в `TenantConfig`/JSON-экспорте/PATCH и скриншот-экспорте этих данных нет
  (`jsonHasSim=false` после всех прогонов).
- Fallback: при отсутствии ack за 1.5 с — статус «сервер/виджет не поддерживает
  симуляцию», без реальных комнат/WS (проверено stub-embed без ack).

### 12.2. Результаты 1/2/9/25/50

| count | 1440: плитки / счётчик | 390: плитки / счётчик | ack, мс | overflow |
|---|---|---|---|---|
| 1 | 1 / 1 | 1 / 1 | ~395/374 | нет |
| 2 | 2 / 2 | 2 / 2 | ~382/374 | нет |
| 9 | 9 / 9 | 9 / 9 | ~385/388 | нет |
| 25 | 25 / 25 | 25 / 25 | ~400/386 | нет |
| 50 | 50 / 50 | 50 / 50 | ~403/436 | нет |

- Participants panel при 9: **9 строк**, счётчик 9, панель открывается.
- `grid`/`spotlight`, `bar`/`island`/`minimal` при 9: плитки 9, счётчик 9, overflow нет.
- DOM: 372→1548 узлов (1440), 402→2362 (390) — виртуализация/лимит виджета
  выдерживает 50 без лагов (ack <0.45 с).
- Типографика панели/статуса: light/dark/390 — шрифтов <12px и контраста <4.5 — **0**;
  горизонтального overflow страницы редактора нет (390: scrollWidth=390).
- Скриншоты: `sim-1440-01/02/09/25/50.png`, `sim-390-*`, `sim-page-*`,
  `sim-spotlight-9.png`, `sim-island-9.png`, `sim-minimal-9.png`,
  `sim-participants-9.png`, `sim-panel-1440-light/dark.png`, `sim-panel-390-light.png`.

### 12.3. Изменённые файлы

| Файл | Что |
|---|---|
| `src/preview.ts` | панель «Тест-сценарий», localStorage per tenant, `preview=1` в URL, postMessage `agil:simulate-participants`, ack/timeout/fallback, caption вне конфига |
| `src/styles.css` | стили sim-панели/caption, 12px токены, mobile full-width панель |
| `docs/editor.md` | раздел «Тест-сценарий» |


---

## 14. Мобильный ретест вёрстки редактора (13.11.2026)

Стенд: локальная prod-симуляция (`@agil/calls-server` + `apps/editor/dist` + demo,
`calls.test:8799`), headless Chrome 390×844 / 768×1024 / 1440×900, light/dark.
Артефакт: [`artifacts/mobile-retest.json`](artifacts/mobile-retest.json); скриншоты:
`screens/mobile-retest-*.png`.

### 14.1. Проблемы и исправления

- **Шапка обрезана, ссылки за экраном.** Причина: компактный режим включался только
  на ≤720px, а на 768 meta выходила за viewport (page overflow), на 390 бренд
  сжимался, ссылки уезжали. Исправлено: единый компактный header на `≤1100px`
  (одна строка 60px, meta с `overflow-x:auto`, скрыты subtitle/draft/tenant-pill,
  бренд `flex:0 0 auto`), на `≤720px` — короткие подписи ссылок
  (`data-short`: API/OpenAPI/Guide/API.md), все 4 видны на 390. Page overflow:
  **390/768/1440 — false**; ссылки: 4/4 visible на всех ширинах.
- **Save dock перекрывал «Бренд».** На `≤1200px` док fixed; добавлены
  `ResizeObserver` → `--save-dock-height`, `.app-body padding-bottom = dock + 28px`,
  `html scroll-padding-bottom`; док скрывается (`is-offscreen`), пока правая
  колонка не во viewport, поэтому не накрывает Brand/превью, а у настроек
  сохранения становится видимым. Последний контент внизу: `lastBottom=719` при
  `dock.top=750` (390) — не перекрыт. Tab-фокус: 55 переходов, **0** элементов под
  доком (focusin поднимает поле выше дока).
- **Превью и панели.** На 390: pane-left (Brand) → preview → pane-right; page
  overflow false; preview 370×439, док не перекрывает его (offscreen при прокрутке
  к превью), на 768/1440 — штатная трёхколоночная сетка без наложений.
- Light/dark: header/dock/секции читаемы, контраст ≥4.5 (проверялось ранее +
  скриншоты).

### 14.2. Замеры

| Ширина | Header | Ссылки | Page overflow | Dock |
|---|---|---|---|---|
| 390 | 60px, meta 226/246 scroll | 4/4 visible (short) | false | offscreen на Brand; 84px у правой колонки, контент выше |
| 768 | 60px, meta 592/592 | 4/4 visible (full) | false | offscreen на Brand; 53px у правой колонки |
| 1440 | 60px | 4/4 visible | false | sticky, 89px, без overlay |

Скриншоты: `mobile-retest-390-header-light.png`, `mobile-retest-390-brand-light.png`,
`mobile-retest-390-brand-dark.png`, `mobile-retest-390-save.png`,
`mobile-retest-390-preview.png`, `mobile-retest-390-focus.png`,
`mobile-retest-768-header-light.png`, `mobile-retest-768-bottom.png`,
`mobile-retest-1440.png`.

### 14.3. Файлы

`src/styles.css` (header ≤1100/≤720, dock-aware spacing, скрытие дока),
`src/inspector.ts` (ResizeObserver высоты дока, IntersectionObserver видимости,
focus-above-dock), `index.html` (`data-short` подписи ссылок), `docs/editor.md`.
