WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-573 · Дефект · Инфраструктура · web

P1 [интеграции]: подключение Google Drive молча возвращает «Не подключено», если пользователь не поставил галочку доступа

На проверке P1 · важно ведёт: —
Суть
# WEB-573 — [интеграции] подключение Google Drive молча возвращает «Не подключено» — починить и доказать живым Google-прогоном

## 1. Суть
Подключение Google Drive обязано либо подключаться, либо ЯВНО говорить, что пошло не так (отмена / нет scope / ошибка обмена), а не молча возвращать «Не підключено».

## 2. Где мы сейчас (13.09.2026)
Статус review, P1, bug. Настоящая причина найдена 08.09: колбэк /api/integrations/google/auth давал 301 на тот же путь, второй заход тратил одноразовый код Google → 400 → «Не підключено». Фикс: одноразовый обмен кода + разбор трёх случаев (отмена / согласие без scope / ошибка обмена) + видимое уведомление; возвращены поля ручного импорта (ссылка на папку/файл, токены) (волна 2960 GO, фикс a9b9a9cc0). Посажено l115e (11.09 01:59Z). QA: offline-харнесс RU/UK/EN 14/14 (волна 3471), source integration GO (3490, ba4aa1c2). Живой прогон с РЕАЛЬНЫМ Google OAuth-провайдером НЕ проведён.

## 3. Хронология
- 08.09: поймано владельцем; первый диагноз (пустой чекбокс) опровергнут — истинная причина 301→400 по журналу боевого nginx.
- 08.09 19:30: волна 2960 GO, фикс a9b9a9cc0.
- 10.09: волны 3382/3416/3437 (source QA Drive); THREAD-STATUS: 3437 source GO, но изменены IntegrationsModal/googleConsentCopy.
- 11.09: 3471 независимая source/component QA GO 14/14; 3490 source integration GO; посадки l115d/l115e.
- 12.09: МОЙКА 3568 — KEEP (живая проверка с настоящим Google не сделана).
- 13.09 08:06Z: разбор 3633 — подключение починено, осталось проверить с настоящим Google.

## 4. Карта документов и кода
- Код: IntegrationsModal.tsx (317, 466, 501, 1380), oauthResultIsolation.ts (374, 401), oauthIntegrationPersistence.ts:53, route.ts:88, googleIds.ts:22.
- Фикс: a9b9a9cc0.
- QA: offline-харнесс RU/UK/EN (12 отказов, 3 success, 3 unconfirmed); checkpoint-3478/snapshot.json.

## 5. Остаток
1. Живой прогон с реальным Google OAuth-провайдером: путь 1 (полный успех → «Підключено», видны поля ручного импорта) + три негатива (отмена / согласие без scope / ошибка обмена), без 301→400 в Network — исполнительская/владелец волна.
2. feature-browser 3503 — pending.
3. Чужое имя «wool2.online» на экране Google — WEB-561 (переезд домена); исторический ремонт 351/135 — WEB-593; оба НЕ здесь.

## 6. Критерий закрытия
Живой прогон с реальным Google: полный успех даёт «Підключено» + поля ручного импорта; три негатива дают внятные сообщения; в Network нет 301→400 на /api/integrations/google/auth; scope содержит drive-scope.

---

## История (прежнее тело, сохранено по канону WEB-449)

## СЕЙЧАС

Владелец 08.09 не смог подключить Google Drive: после прохождения экранов Google его возвращает в ту же панель «Документи та файли» с надписью **«Не підключено»**, без единого слова о том, что пошло не так.

## Причина, видная на его скриншотах

На третьем экране Google — «Выберите, какие права доступа вы хотите предоставить приложению» — чекбокс **«Просмотр и скачивание всех файлов на Google Диске» стоит ПУСТЫМ**. Google выдаёт разрешения по одному (granular consent) и по умолчанию не даёт ничего. Пользователь нажимает «Продолжить», приложение получает вход БЕЗ scope на диск, и подключение молча не происходит.

То есть пользователь сделал всё «правильно» с его точки зрения и не получил ни результата, ни объяснения.

## Что чинить

1. **Распознавать частичное согласие.** После возврата с OAuth приложение обязано проверить, какие scope реально выданы. Если нужного scope нет — сказать это ЯВНО: «вы не дали доступ к файлам Google Диска, поставьте галочку и попробуйте снова». Молчаливый возврат в то же окно — сам по себе дефект.
2. **Отличать три случая друг от друга:** пользователь отменил; пользователь согласился, но без нужного scope; произошла ошибка обмена кода на токен. Сейчас все три выглядят одинаково.
3. **Подсказка ДО перехода.** На кнопке подключения предупредить, что на экране Google нужно поставить галочку доступа к файлам — иначе подключение не состоится.

## Смежное: чужое имя на экране Google

На экране согласия написано **«Вход в сервис "wool2.online"»**, хотя пользователь находится на **sixbyy.com**. Старое имя осталось в настройках проекта Google. Это пугает пользователя (выглядит как чужое приложение) и мешает верификации. Относится к переезду на новый домен — см. эпик WEB-561.

Отдельно Google показывает «Эксперты Google не проверяли это приложение» — приложение в тестовом режиме, верификация не пройдена. Это отдельная работа владельца в кабинете Google.

## KNOWN ISSUES / ТРАБЛШУТИНГ

- **Симптом:** после экранов Google панель показывает «Не подключено», ошибок нет.
- **Проверка:** пройти поток и НЕ ставить галочку доступа к файлам — воспроизводится.
- **Причина:** granular consent Google, пустой чекбокс по умолчанию; приложение не проверяет выданные scope.
- **Обходной путь для пользователя прямо сейчас:** поставить галочку «Просмотр и скачивание всех файлов на Google Диске» перед «Продолжить».

## ДЛЯ ТИКЕТА

Заведено координатором 08.09.2026 по скриншотам владельца. Владелец спросил дословно: «что ты от меня ждал?» — и это точный диагноз проблемы: интерфейс ничего от него не ждал вслух.


FILL-2104:3382:active
Проверено 2026-09-10T21:10:06.758266+00:00. Волна 3382, A1, состояние active. Живая tmux pane и свежий журнал подтверждены. Exact BASE aaad22f1312a5cae4aff951cc927df01afc27706. Входы SHA256 и bundle ref проверены перед dispatch. Бриф Intel nc-ops-scripts/fill-slots-20260910-2104/3382-acc-drive-consent-brief.md. Сдача /home/wave/waves/3382ACCDRIVECONSENT-REPORT.md. Посадки/окончательной приёмки этой работы нет.

REFILL-20260910-2155-3416
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3416 / drive-consent-localization на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 64 с. Входы /home/ubuntu/waves/inputs/3416-drive-consent-localization; SHA256 и exact BASE d5c1658bd09f7818b4c6bcda942c52fadec0a03f проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.

REFILL-2221-RESULTS-WEB-573
Снимок 2026-09-10T22:27:17.204978+00:00

Волна 3437 m4 gpt-5.6-luna: active (дочерний процесс подтверждён, журнал 1 с); exact BASE f8e63a791e19cd8024b1e007d30a029a180d74ad; inputs /Users/milamarty/waves/inputs/3437-acc-drive-localization
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.

REFILL3472:WEB-573
2026-09-10T23:16:44.018057+00:00
Волна 3471 neo active; acc-drive-qa-product-change; BASE 9aaeaee30b3c4ec69d6630d4fba1d0bc69736da8
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web

CHECKPOINT3478:WEB-573 2026-09-10T23:30:08.650195+00:00
3471 независимая source/component QA GO: 14/14; реальный IntegrationsModal выполнен через offline harness RU/UK/EN; 12 отказов, 3 success, 3 unconfirmed. Product и inherited tests не менялись. Live Google/nginx/browser/release pending.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.

---

## история (тело до 13.09.2026)

## СЕЙЧАС

Владелец 08.09 не смог подключить Google Drive: после прохождения экранов Google его возвращает в ту же панель «Документи та файли» с надписью **«Не підключено»**, без единого слова о том, что пошло не так.

## Причина, видная на его скриншотах

На третьем экране Google — «Выберите, какие права доступа вы хотите предоставить приложению» — чекбокс **«Просмотр и скачивание всех файлов на Google Диске» стоит ПУСТЫМ**. Google выдаёт разрешения по одному (granular consent) и по умолчанию не даёт ничего. Пользователь нажимает «Продолжить», приложение получает вход БЕЗ scope на диск, и подключение молча не происходит.

То есть пользователь сделал всё «правильно» с его точки зрения и не получил ни результата, ни объяснения.

## Что чинить

1. **Распознавать частичное согласие.** После возврата с OAuth приложение обязано проверить, какие scope реально выданы. Если нужного scope нет — сказать это ЯВНО: «вы не дали доступ к файлам Google Диска, поставьте галочку и попробуйте снова». Молчаливый возврат в то же окно — сам по себе дефект.
2. **Отличать три случая друг от друга:** пользователь отменил; пользователь согласился, но без нужного scope; произошла ошибка обмена кода на токен. Сейчас все три выглядят одинаково.
3. **Подсказка ДО перехода.** На кнопке подключения предупредить, что на экране Google нужно поставить галочку доступа к файлам — иначе подключение не состоится.

## Смежное: чужое имя на экране Google

На экране согласия написано **«Вход в сервис "wool2.online"»**, хотя пользователь находится на **sixbyy.com**. Старое имя осталось в настройках проекта Google. Это пугает пользователя (выглядит как чужое приложение) и мешает верификации. Относится к переезду на новый домен — см. эпик WEB-561.

Отдельно Google показывает «Эксперты Google не проверяли это приложение» — приложение в тестовом режиме, верификация не пройдена. Это отдельная работа владельца в кабинете Google.

## KNOWN ISSUES / ТРАБЛШУТИНГ

- **Симптом:** после экранов Google панель показывает «Не подключено», ошибок нет.
- **Проверка:** пройти поток и НЕ ставить галочку доступа к файлам — воспроизводится.
- **Причина:** granular consent Google, пустой чекбокс по умолчанию; приложение не проверяет выданные scope.
- **Обходной путь для пользователя прямо сейчас:** поставить галочку «Просмотр и скачивание всех файлов на Google Диске» перед «Продолжить».

## ДЛЯ ТИКЕТА

Заведено координатором 08.09.2026 по скриншотам владельца. Владелец спросил дословно: «что ты от меня ждал?» — и это точный диагноз проблемы: интерфейс ничего от него не ждал вслух.


FILL-2104:3382:active
Проверено 2026-09-10T21:10:06.758266+00:00. Волна 3382, A1, состояние active. Живая tmux pane и свежий журнал подтверждены. Exact BASE aaad22f1312a5cae4aff951cc927df01afc27706. Входы SHA256 и bundle ref проверены перед dispatch. Бриф Intel nc-ops-scripts/fill-slots-20260910-2104/3382-acc-drive-consent-brief.md. Сдача /home/wave/waves/3382ACCDRIVECONSENT-REPORT.md. Посадки/окончательной приёмки этой работы нет.

REFILL-20260910-2155-3416
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3416 / drive-consent-localization на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 64 с. Входы /home/ubuntu/waves/inputs/3416-drive-consent-localization; SHA256 и exact BASE d5c1658bd09f7818b4c6bcda942c52fadec0a03f проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.

REFILL-2221-RESULTS-WEB-573
Снимок 2026-09-10T22:27:17.204978+00:00

Волна 3437 m4 gpt-5.6-luna: active (дочерний процесс подтверждён, журнал 1 с); exact BASE f8e63a791e19cd8024b1e007d30a029a180d74ad; inputs /Users/milamarty/waves/inputs/3437-acc-drive-localization
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.

REFILL3472:WEB-573
2026-09-10T23:16:44.018057+00:00
Волна 3471 neo active; acc-drive-qa-product-change; BASE 9aaeaee30b3c4ec69d6630d4fba1d0bc69736da8
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web

CHECKPOINT3478:WEB-573 2026-09-10T23:30:08.650195+00:00
3471 независимая source/component QA GO: 14/14; реальный IntegrationsModal выполнен через offline harness RU/UK/EN; 12 отказов, 3 success, 3 unconfirmed. Product и inherited tests не менялись. Live Google/nginx/browser/release pending.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.
Доказательства

FILL-2104:3382:active
Проверено 2026-09-10T21:10:06.758266+00:00. Волна 3382, A1, состояние active. Живая tmux pane и свежий журнал подтверждены. Exact BASE aaad22f1312a5cae4aff951cc927df01afc27706. Входы SHA256 и bundle ref проверены перед dispatch. Бриф Intel nc-ops-scripts/fill-slots-20260910-2104/3382-acc-drive-consent-brief.md. Сдача /home/wave/waves/3382ACCDRIVECONSENT-REPORT.md. Посадки/окончательной приёмки этой работы нет.

REFILL-20260910-2155-3416
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3416 / drive-consent-localization на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 64 с. Входы /home/ubuntu/waves/inputs/3416-drive-consent-localization; SHA256 и exact BASE d5c1658bd09f7818b4c6bcda942c52fadec0a03f проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.

REFILL-2221-RESULTS-WEB-573
Снимок 2026-09-10T22:27:17.204978+00:00

Волна 3437 m4 gpt-5.6-luna: active (дочерний процесс подтверждён, журнал 1 с); exact BASE f8e63a791e19cd8024b1e007d30a029a180d74ad; inputs /Users/milamarty/waves/inputs/3437-acc-drive-localization
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.

THREAD-STATUS-2240 3437 source GO, но в ходе QA изменены IntegrationsModal и googleConsentCopy. Изменённые исходники требуют независимой проверки; полной готовности к посадке нет.

REFILL3472:WEB-573
2026-09-10T23:16:44.018057+00:00
Волна 3471 neo active; acc-drive-qa-product-change; BASE 9aaeaee30b3c4ec69d6630d4fba1d0bc69736da8
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web

CHECKPOINT3478:WEB-573 2026-09-10T23:30:08.650195+00:00
3471 независимая source/component QA GO: 14/14; реальный IntegrationsModal выполнен через offline harness RU/UK/EN; 12 отказов, 3 success, 3 unconfirmed. Product и inherited tests не менялись. Live Google/nginx/browser/release pending.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.

CHECKPOINT3490:WEB-573 2026-09-11T00:21:57.481659+00:00
3490 source integration confirmed running on Neo. Three exact accepted heads: Drive3471 28fcffa807178fc31ffc0841c92f9f1f2b90998f; sync3412 aa098fc84838c524372b36eafb46f0199e0209ed; planner3470 cb8aadd00f39ec63ba40bc613a3865108420d356. Inputs/report/bundle SHA and git objects verified before dispatch. Merge verdict pending. Historical documents not repaired; Google browser/runtime and sync release checks pending. Editor3489 excluded. Evidence: wash-integration-3490/3490-receipt.json, inputs/manifest.json and full input reports.

CHECKPOINT3492:WEB-573 2026-09-11T00:32:55.179360+00:00
3490 source integration GO ba4aa1c219625df95035cd1eb9653d9a6b0c4903: три точных принятых головы, ноль конфликтов, Drive14/14 + sync17/17 + planner10/10. Lint 0 errors/19 inherited warnings. Exact bundle/head/clean/SHA проверены и скопированы; SHA 9a47642243fa5412fa647c5a1ff23f2ddd0e96f493a5eb5dd58c2938a1218cac. Browser/provider/live/release pending. Planner dry-run only; исторические351/135 документов не восстановлены. Evidence: checkpoint-3492/snapshot.json, полный3490 REPORT, Neo3490WASHINTEGRATION-evidence.

L115DBUILDSTART:WEB-573 2026-09-11T00:42:10.710873+00:00
l115d build started 2026-09-11T00:39:55Z on A2, PID2866558, clean dedicated /home/ubuntu/wt-l115d from verified3490 HEAD ba4aa1c219625df95035cd1eb9653d9a6b0c4903. Private dependencies copied, 281 generated wrappers rebased, 13 NEXT_PUBLIC values read from both live processes and equal. Prisma generation, P19 compatibility and schema3017 fields passed; activation generated; prebuild running at00:41Z. Cache archived to M4Ext with SHA302d195a5bb26234105aa8fa9e1e0613e96381dc730babd65b3ed037e227770f before removal. No PACK_OK, landing or browser acceptance yet. Source includes Drive/sync/planner only; editor3492 and capacity3491 remain separate. Evidence: checkpoint-3492/l115d-build-start.json, l115d-preflight.log, build-cache-archive.json; A2 /home/ubuntu/l115d-chain.log.

LANDING115D-PG3496:WEB-573 2026-09-11T01:15:45.387983+00:00
l115d deployed 2026-09-11 01:13 UTC: source ba4aa1c219625df95035cd1eb9653d9a6b0c4903, artifact SHA256 446245699e4e28f03336d7726d9fa4dc422f8a8e4b7597d572b29bd1e0eb57f0. Signature positive/negative checks passed. Both backends paidReady=true/enforce_ready with exact source; four service path gate passed; live chat HTTP200; distance route HTTP200 and asks for missing origin. Browser opened one of58 documents, editor surfaces present, zero new errors after click; initial load retained two aborted cookies RSC requests and admin/agents HTTP403 plus console error. SIP readiness200 and LEASE_LOST0. Ships wash3490 Drive/sync/planner; editor3495 remains separate. Historical351/135 document repair and full feature-specific provider/browser QA remain open. Rollback units: /home/ubuntu/prod/shared/pre-l115d-units; previous release l115c. Evidence: checkpoint-3495/l115d-{dryrun,flip,live-postqa,editor-postqa}.log.

LANDING115E:WEB-573 2026-09-11T02:01:10.157440+00:00
l115e landed 2026-09-11 01:59 UTC. Source bb43b5fe20398309b3b373df3c00fa1d3040397d; artifact SHA256 156323c694a6854b6207ee2b6dcd101abba99b67c384cc6282e10d098c9eb357. Both live backends independently confirmed exact source, ready=true and paidReady=true/enforce_ready at 02:00 UTC. Build/signature/negative signature/dryrun/four-unit path gates passed; chat and distance HTTP200. Browser opened one of58 fixture documents, editor surfaces present, zero new errors after opening. Initial load recorded three aborted RSC requests and admin/agents403 plus console error; clean console not claimed. SIP readiness200, LEASE_LOST0, no SIP restart. Ships accepted editor3495 atop wash3490. Feature-specific browser3503, real provider OAuth, historical351/135 repair, capacity and Security26 remain open. Rollback: prod/shared/pre-l115e-units to l115d. Evidence: release-l115e/l115e-{dryrun,flip,live-postqa,editor-postqa}.log.
Лента
2026-09-08T15:44:32.685Z · coordinator
[08.09 координатор — НАСТОЯЩАЯ ПРИЧИНА, предыдущее объяснение неверно]

**Мой первый диагноз (пустой чекбокс согласия) НЕВЕРЕН.** Владелец галочку поставил, и в журнале видно, что доступ к диску в запросе присутствует (`scope=…drive…`). Ломается позже.

## Причина по журналу боевого nginx

Три попытки владельца, все три одинаково:

```
15:37:05  GET /api/integrations/google/auth?state=…&code=…&scope=…drive…  → 301  (178 байт)
15:37:06  тот же адрес                                                     → 400  (3457 байт)

15:41:08  GET /api/integrations/google/auth?state=10980773…&code=…&scope=…drive…  → 301
15:41:09  тот же адрес                                                            → 400

15:41:59  GET /api/integrations/google/auth?state=709e3a55…&code=…&scope=…drive…  → 301
15:41:59  тот же адрес                                                            → 400
```

**Код авторизации Google одноразовый.** Первый запрос его тратит, ответ 301 приводит браузер по тому же адресу второй раз — и обменивать уже нечего. Второй заход отдаёт 400, интерфейс показывает «Не підключено».

Подключение сейчас невозможно в принципе, независимо от действий пользователя.

## Что чинить

1. **Убрать 301 на пути колбэка.** Обмен кода должен происходить ровно один раз. Найти, кто выдаёт этот редирект — nginx-нормализация хоста/слэша или сам обработчик — и назвать как файл:строка либо как правило конфигурации.
2. Если редирект нужен по продуктовым причинам, он обязан идти **после** успешного обмена и **на другой адрес**, а не на тот же путь с тем же кодом.
3. Повторное предъявление уже потраченного кода не должно выглядеть как «не подключено» — это отдельное состояние с внятным сообщением.

## Вторая пропажа (сообщение владельца 08.09)

> «меню гугл драйва было полнее раньше, там даже вставить можно было ссылку на гугл папку или файл, и токен кажется. А сейчас тут ничего нельзя»

В панели «Документи та файли» пропали: поле для вставки ссылки на папку или файл Google и ввод токена. Сейчас доступна только кнопка подключения. Проверить, когда именно это исчезло — возможно, вместе с переездом на новый домен (см. WEB-561) — и вернуть.

## Что НЕ является причиной

- Чекбокс согласия: владелец его отметил, scope выдан.
- Отсутствие верификации приложения в Google: экран «Эксперты Google не проверяли это приложение» на тестовом режиме нормален и обмену кода не мешает.
2026-09-08T19:30:25.644Z ·
Волна 2960 закончила, VERDICT=GO. Две причины. (1) При заблокированном всплывающем окне возврат шёл на корень и не переоткрывал окно интеграций — IntegrationsModal.tsx:317 + oauthResultIsolation.ts:374; лечение — восстановительный URL и перемонтирование AppShell (oauthResultIsolation.ts:401, AppShell.tsx:212). (2) При неудаче сохранения токена код МОЛЧА выходил без причины на экране — IntegrationsModal.tsx:466; лечение — разбор серверной причины (oauthIntegrationPersistence.ts:53), журналирование маршрута (route.ts:88) и видимое уведомление (IntegrationsModal.tsx:501). Поля ссылки на папку/файл и ручные токены удалены НЕ намеренно: ARCHITECTURE.md:323 и INTEGRATIONS_SMOKE.md:28 описывают их как рабочие, решения об удалении в истории нет — восстановлены (IntegrationsModal.tsx:1380, валидация googleIds.ts:22). Фикс a9b9a9cc0. Ждёт посадки в l113x.
2026-09-08T19:30:32.173Z ·
Волна 2960 закончила, VERDICT=GO. Две причины. (1) При заблокированном всплывающем окне возврат шёл на корень и не переоткрывал окно интеграций — IntegrationsModal.tsx:317 + oauthResultIsolation.ts:374; лечение — восстановительный URL и перемонтирование AppShell (oauthResultIsolation.ts:401, AppShell.tsx:212). (2) При неудаче сохранения токена код МОЛЧА выходил без причины на экране — IntegrationsModal.tsx:466; лечение — разбор серверной причины (oauthIntegrationPersistence.ts:53), журналирование маршрута (route.ts:88) и видимое уведомление (IntegrationsModal.tsx:501). Поля ссылки на папку/файл и ручные токены удалены НЕ намеренно: ARCHITECTURE.md:323 и INTEGRATIONS_SMOKE.md:28 описывают их как рабочие, решения об удалении в истории нет — восстановлены (IntegrationsModal.tsx:1380, валидация googleIds.ts:22). Фикс a9b9a9cc0. Ждёт посадки в l113x.
2026-09-10T18:55:03.484Z · coordinator
BOARD-WASH-20260910:WAVE-3330
По поручению владельца 18:43Z назначена исполнительская волна 3330 (ui), Codex gpt-5.6-luna xhigh, M4. Полная история карточки и база l115c 1ad52e16b доставлены, brief-guard и проверка Git-базы пройдены. Задача: проверить существующую сдачу, устранить остатки, передать бандл и доказательства. Финальная приёмка и посадка остаются за координатором. Запуск группы подтверждён в журнале диспетчера.
КАРТА ДОКУМЕНТОВ: ноут /Users/annakorin/nc-ops-scripts/board-wash-20260910/3330-wash-ui-brief.md; M4 /Users/milamarty/waves/inputs/board-wash-20260910/ui-tickets.json; ожидаемый отчёт /Users/milamarty/waves/3330WASHUI-REPORT.md. Правила обогащения: WEB-449.
2026-09-10T21:10:32.334Z · coordinator
FILL-2104:3382:active
Проверено 2026-09-10T21:10:06.758266+00:00. Волна 3382, A1, состояние active. Живая tmux pane и свежий журнал подтверждены. Exact BASE aaad22f1312a5cae4aff951cc927df01afc27706. Входы SHA256 и bundle ref проверены перед dispatch. Бриф Intel nc-ops-scripts/fill-slots-20260910-2104/3382-acc-drive-consent-brief.md. Сдача /home/wave/waves/3382ACCDRIVECONSENT-REPORT.md. Посадки/окончательной приёмки этой работы нет.
2026-09-10T21:55:31.071Z · coordinator
REFILL-20260910-2155-3416
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3416 / drive-consent-localization на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 64 с. Входы /home/ubuntu/waves/inputs/3416-drive-consent-localization; SHA256 и exact BASE d5c1658bd09f7818b4c6bcda942c52fadec0a03f проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
2026-09-10T22:27:17.814Z · coordinator
REFILL-2221-RESULTS-WEB-573
Снимок 2026-09-10T22:27:17.204978+00:00

Волна 3437 m4 gpt-5.6-luna: active (дочерний процесс подтверждён, журнал 1 с); exact BASE f8e63a791e19cd8024b1e007d30a029a180d74ad; inputs /Users/milamarty/waves/inputs/3437-acc-drive-localization
Новой посадки этим тиком нет. Evidence: nc-ops-scripts/refill-20260910-2221/.
2026-09-10T22:53:37.973Z · coordinator
THREAD-STATUS-2240 3437 source GO, но в ходе QA изменены IntegrationsModal и googleConsentCopy. Изменённые исходники требуют независимой проверки; полной готовности к посадке нет.
2026-09-10T23:18:14.080Z · coordinator
REFILL3472:WEB-573
2026-09-10T23:16:44.018057+00:00
Волна 3471 neo active; acc-drive-qa-product-change; BASE 9aaeaee30b3c4ec69d6630d4fba1d0bc69736da8
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web
2026-09-10T23:33:43.941Z · coordinator
CHECKPOINT3478:WEB-573 2026-09-10T23:30:08.650195+00:00
3471 независимая source/component QA GO: 14/14; реальный IntegrationsModal выполнен через offline harness RU/UK/EN; 12 отказов, 3 success, 3 unconfirmed. Product и inherited tests не менялись. Live Google/nginx/browser/release pending.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.
2026-09-11T00:21:58.149Z · coordinator
CHECKPOINT3490:WEB-573 2026-09-11T00:21:57.481659+00:00
3490 source integration confirmed running on Neo. Three exact accepted heads: Drive3471 28fcffa807178fc31ffc0841c92f9f1f2b90998f; sync3412 aa098fc84838c524372b36eafb46f0199e0209ed; planner3470 cb8aadd00f39ec63ba40bc613a3865108420d356. Inputs/report/bundle SHA and git objects verified before dispatch. Merge verdict pending. Historical documents not repaired; Google browser/runtime and sync release checks pending. Editor3489 excluded. Evidence: wash-integration-3490/3490-receipt.json, inputs/manifest.json and full input reports.
2026-09-11T00:32:56.102Z · coordinator
CHECKPOINT3492:WEB-573 2026-09-11T00:32:55.179360+00:00
3490 source integration GO ba4aa1c219625df95035cd1eb9653d9a6b0c4903: три точных принятых головы, ноль конфликтов, Drive14/14 + sync17/17 + planner10/10. Lint 0 errors/19 inherited warnings. Exact bundle/head/clean/SHA проверены и скопированы; SHA 9a47642243fa5412fa647c5a1ff23f2ddd0e96f493a5eb5dd58c2938a1218cac. Browser/provider/live/release pending. Planner dry-run only; исторические351/135 документов не восстановлены. Evidence: checkpoint-3492/snapshot.json, полный3490 REPORT, Neo3490WASHINTEGRATION-evidence.
2026-09-11T00:42:11.214Z · coordinator
L115DBUILDSTART:WEB-573 2026-09-11T00:42:10.710873+00:00
l115d build started 2026-09-11T00:39:55Z on A2, PID2866558, clean dedicated /home/ubuntu/wt-l115d from verified3490 HEAD ba4aa1c219625df95035cd1eb9653d9a6b0c4903. Private dependencies copied, 281 generated wrappers rebased, 13 NEXT_PUBLIC values read from both live processes and equal. Prisma generation, P19 compatibility and schema3017 fields passed; activation generated; prebuild running at00:41Z. Cache archived to M4Ext with SHA302d195a5bb26234105aa8fa9e1e0613e96381dc730babd65b3ed037e227770f before removal. No PACK_OK, landing or browser acceptance yet. Source includes Drive/sync/planner only; editor3492 and capacity3491 remain separate. Evidence: checkpoint-3492/l115d-build-start.json, l115d-preflight.log, build-cache-archive.json; A2 /home/ubuntu/l115d-chain.log.
2026-09-11T01:15:45.929Z · coordinator
LANDING115D-PG3496:WEB-573 2026-09-11T01:15:45.387983+00:00
l115d deployed 2026-09-11 01:13 UTC: source ba4aa1c219625df95035cd1eb9653d9a6b0c4903, artifact SHA256 446245699e4e28f03336d7726d9fa4dc422f8a8e4b7597d572b29bd1e0eb57f0. Signature positive/negative checks passed. Both backends paidReady=true/enforce_ready with exact source; four service path gate passed; live chat HTTP200; distance route HTTP200 and asks for missing origin. Browser opened one of58 documents, editor surfaces present, zero new errors after click; initial load retained two aborted cookies RSC requests and admin/agents HTTP403 plus console error. SIP readiness200 and LEASE_LOST0. Ships wash3490 Drive/sync/planner; editor3495 remains separate. Historical351/135 document repair and full feature-specific provider/browser QA remain open. Rollback units: /home/ubuntu/prod/shared/pre-l115d-units; previous release l115c. Evidence: checkpoint-3495/l115d-{dryrun,flip,live-postqa,editor-postqa}.log.
2026-09-11T02:01:10.773Z · coordinator
LANDING115E:WEB-573 2026-09-11T02:01:10.157440+00:00
l115e landed 2026-09-11 01:59 UTC. Source bb43b5fe20398309b3b373df3c00fa1d3040397d; artifact SHA256 156323c694a6854b6207ee2b6dcd101abba99b67c384cc6282e10d098c9eb357. Both live backends independently confirmed exact source, ready=true and paidReady=true/enforce_ready at 02:00 UTC. Build/signature/negative signature/dryrun/four-unit path gates passed; chat and distance HTTP200. Browser opened one of58 fixture documents, editor surfaces present, zero new errors after opening. Initial load recorded three aborted RSC requests and admin/agents403 plus console error; clean console not claimed. SIP readiness200, LEASE_LOST0, no SIP restart. Ships accepted editor3495 atop wash3490. Feature-specific browser3503, real provider OAuth, historical351/135 repair, capacity and Security26 remain open. Rollback: prod/shared/pre-l115e-units to l115d. Evidence: release-l115e/l115e-{dryrun,flip,live-postqa,editor-postqa}.log.
2026-09-12T22:17:55.130Z · coordinator
[12.09 22:17Z координатор] МОЙКА 12.09 (3568-wash-g3-editor-ui): фикс OAuth (одноразовый код + 301) и возврат ручного импорта посажены в l115e, код в дереве. Но живая проверка с настоящим Google ещё не сделана (только офлайн-харнесс), и исторические документы не починены. KEEP.
Остаток: Живой прогон с РЕАЛЬНЫМ Google OAuth-провайдером не проведён (только offline-харнесс RU/UK/EN: 12 отказов, 3 success, 3 unconfirmed).; Исторический ремонт 351/135 документов не выполнен (отдельная работа).; Чужое имя «wool2.online» на экране Google — пункт WEB-561 (переезд домена), здесь не чинится.
Отчёт: /Users/milamarty/waves/3568WASH*-REPORT.md (M4), копия nc-ops-scripts/shift-20260912-resume/wash-20260912/collected/. Проверка по исходнику прода l115g (9c8a9762).
2026-09-13T08:06:04.381Z · coordinator
[13.09 08:06Z координатор] РАЗБОР 13.09 (волна 3634, DeepSeek): Подключение Гугл-диска починили. Раньше сервер перебрасывал браузер по кругу, «съедал» одноразовый ключ от Гугла и тихо показывал «не подключено». Теперь он меняет ключ один раз, а если что-то не так — говорит словами: «вы отменили», «вы не дали доступ к файлам» или «не смогли обменять ключ», и заранее подсказывает поставить галочку. Осталось один раз проверить с настоящим Гуглом на живом сайте: подключить и увидеть «подключено», и отдельно три случая, когда Гугл отказывает. Старые 351 документ не трогаем — это другая карточка.
Остаток: Живой прогон с реальным Google OAuth-провайдером не проведён (только offline-харнесс RU/UK/EN: 12 отказов, 3 success, 3 unconfirmed).; Исторический ремонт 351/135 документов не выполнен — это WEB-593, здесь не чинить.; Имя «wool2.online» на экране Google — WEB-561 (переезд домена), здесь не чинить.; feature-browser 3503 pending.
Предложение: Сценарий живого подключения владельцем: путь 1 (полный успех → «Підключено», видны поля ручного импорта) + три негатива (отмена / согласие без scope / ошибка обмена) с записью финального колбэк-URL, Network-последовательности по /api/integrations/google/auth (нет 301→400) и строки интеграции в БД (scope содержит drive-scope). Без Google — контрактные проверки: oauthResultIsolation (восстановительн
Материалы: shift-20260912-resume/colM2|colP2/ (RESULT.md — ранбуки и спецификации). Статус не меняю.
2026-09-13T10:37:39.894Z · coordinator
[13.09 10:37Z координатор] Мойка доски, волна 3652. RETURN: фикс OAuth 301→400 (волна 2960 GO, a9b9a9cc0) посажен l115e, offline QA 14/14 (3471). Не хватает живого прогона с реальным Google OAuth. Статус review.
2026-09-13T10:59:16.940Z · coordinator
[13.09 10:59Z координатор] Волна 3658 (GO, refs/waves/3658 = 7068c7b1) нашла настоящую причину, и она не там, где искали раньше.
Саму цепочку подключения (получить код, обменять на токен, сохранить) чинили прошлые волны. Непочиненным осталось ЧТЕНИЕ уже сохранённого состояния: при открытии панели интеграций приложение спрашивало сервер «подключено ли», и если этот запрос падал — сеть, временная ошибка базы, битый ответ — экран показывал «Не подключено» вообще без объяснений, даже в журнале ничего не оставалось. Для по-настоящему подключённого человека это выглядело один в один как обрыв связи с Google.
Теперь неудачное чтение состояния — отдельный видимый случай с текстом «не удалось проверить статус подключения, обновите страницу» и записью причины в журнал. «Не подключено» показывается только когда сохранённой записи действительно нет.
Остаток: сквозной прогон с живым согласием Google. Внешняя сеть волнам запрещена, это работа координатора.
2026-09-13T13:52:11.536Z · coordinator
[13.09 13:52Z координатор] НАСТОЯЩАЯ ПРИЧИНА БЛОКИРОВКИ НАЙДЕНА, И ОНА НАША, А НЕ GOOGLE. Владелец прислал снимок: при нажатии «Подключить Google Drive» появляется наше собственное сообщение «Article 28 path closed: confirmed contract and processing controls are required».
Проверил по дереву текущей линии: файл реестра подтверждённых договоров с обработчиками данных `src/content/legal/real/ARTICLE28_PROVIDER_CONFIRMATIONS.json` содержит ПУСТОЙ список. Поэтому заслон по статье 28 закрывает путь к Google Drive — и это ровно то, о чём предупреждала волна 3658 в разделе известных проблем.
Значит карточка блокирована не внешним входом Google, а нашей же настройкой. Чтобы открыть путь, нужна одна подписанная запись в реестре: кто обработчик, какая договаривающаяся сторона, какие меры защиты, на каком основании передача. Запись подписывается ключом — это осознанно сделано так, чтобы её нельзя было тихо подсунуть.
От владельца требуется не три минуты кликов, а ОДНА фраза: подтверждает ли он, что с Google действуют стандартные условия обработки данных. После этого запись готовлю и подписываю я, и кнопка начинает работать.
Карточку из его личного списка кликов убираю — она переезжает в список решений.
2026-09-13T14:51:22.951Z · coordinator
[13.09 14:51Z координатор] ЦЕПОЧКА ЗАМКНУЛАСЬ, И ОНА УПИРАЕТСЯ В ОДНУ ФРАЗУ ВЛАДЕЛЬЦА. Волна 3718 (GO) довела разбор до конца.
Что выяснено по коду и проверено на одноразовой базе:
1. «Поставить в очередь чтения» — это ДВЕ записи сразу: пометка в карточке документа и отдельная строка в таблице заданий, которую читает рабочий. Моя прошлая попытка написала только первую, поэтому рабочий её не видел. Скрипт волны делает обе, и его результат по форме совпадает с тем, что пишет обычная загрузка файла.
2. Но для документов, пришедших с Google Drive, у нас НИГДЕ НЕ СОХРАНЕНЫ САМИ БАЙТЫ ФАЙЛА — ни разу. Значит ставить их в очередь чтения бессмысленно: читать физически нечего, рабочий сразу скажет «файл потерян». Скрипт это понимает и честно отказывается создавать заявку, которую некому выполнить.
3. Единственный настоящий путь для них — заново скачать файлы из Google Drive отдельным инструментом.
А скачивание из Drive у нас сейчас закрыто НАШИМ ЖЕ заслоном по статье 28: реестр подтверждённых договоров пуст (см. WEB-573).
Итого: сто документов вернутся в поиск ровно тогда, когда владелец подтвердит условия обработки данных с Google, я внесу и подпишу запись в реестр, и мы перекачаем файлы. Никакой другой путь их не чинит.
Инструмент постановки в очередь чтения готов и проверен — он пригодится для документов, у которых байты сохранены.
2026-09-13T15:16:27.442Z · coordinator
[13.09 15:16Z координатор] ПОДТВЕРЖДЕНИЕ ВЛАДЕЛЬЦА ПОЛУЧЕНО 13.09 15:14Z: «про Google стандартные условия я тебе давно подтвердил. Подтверждаю.»
Это снимает блокировку с трёх мест сразу: подключение Google Drive, возврат ста десяти документов в поиск (их нечем вернуть, кроме перекачки из Drive) и закрытие эпика защиты данных в части реестра обработчиков.
Что делаю дальше сам, по шагам:
1. Реестр подтверждений подписывается ключом, которого в проекте ЕЩЁ НЕТ: подпись считается кодом обмена с секретом, а сам секрет нигде не заведён. Завожу ключ, кладу в защищённое хранилище координатора и в окружение прода, потому что без него приложение не сможет проверить запись.
2. Беру ссылку на стандартные условия обработки данных Google и её отпечаток — запись пиннится к конкретной версии документа, чтобы подмена документа по тому же адресу не прошла молча.
3. Штатным инструментом проекта добавляю подписанную запись в реестр, в дереве кандидата следующей линии, а не правкой на проде.
4. Собираю и сажаю линию, после чего кнопка подключения Диска работает, и я запускаю перекачку документов.
Ничего из этого не делается руками в обход инструмента: прошлый урок про прямую правку базы записан.
2026-09-13T15:33:45.448Z · coordinator
[13.09 15:33Z координатор] Владелец уточнил 15:31Z: Диск подключается ЛИЧНЫМ гугл-аккаунтом, Workspace нет, компании нет.
Это меняет ответ, и я не стану вписывать запись в реестр. Причина прямая: реестр статьи 28 — это реестр договоров с ОБРАБОТЧИКАМИ, то есть теми, кому мы передаём данные наших пользователей на обработку от своего имени. С личным гугл-аккаунтом такого договора не существует в природе: там действуют обычные пользовательские условия, а не соглашение об обработке. Запись «договор подтверждён» была бы неправдой в юридическом реестре, а такие записи читают всерьёз.
Возникло подозрение, которое надо проверить кодом, а не на ощущениях: возможно, заслон вообще неправильно относит подключение Диска к обработчикам. Когда пользователь подключает СВОЁ хранилище своим согласием и мы читаем ЕГО файлы, Google в этом потоке — не наш субподрядчик по чужим данным, а собственное хранилище пользователя. Это другая правовая роль, и код обязан их различать.
Поставлена волна 3726: перечислить по коду, что именно уходит к Google и чьи это данные, показать строку, где заслон принимает решение, и выбрать один из трёх исходов — подключение Диска законно освобождается от проверки, либо это всё же поток обработчика и тогда личный аккаунт его не закрывает никак, либо нужен третий, более узкий вид записи. Реестр волне трогать запрещено.
Следствие для WEB-593: сто десять документов с Диска остаются невозвратимыми, пока это не решено — им нужна перекачка, а она за этим же заслоном.
2026-09-13T17:37:07.736Z · coordinator
[13.09 17:37Z координатор] ПОСАЖЕНО В ПРОД. Линия l115k, источник `ac4eb8c68d70de829608bcae5a7acef693ab85fb`, артефакт `me2-standalone-linux-arm64-ac4eb8c68-20260913T165733Z` sha256 `eafbeb5146ddf0b498f82d608ddd30d15053d23156b43603dfb88dfe71aaebed`, релиз `arm64-l115k-20260913T165733Z`, переключение 13.09 17:27:44Z.

Проверено после переключения: публичный `https://app.sixbyy.com/` 200, `/api/ready` `ready:true` с коммитом линии, проба базы пройдена, `paidReady:true`, поза `enforce_ready`, обе копии приложения (3010 и 3012) и рабочие таймеры на новом релизе, упавших юнитов нет. Артефакт опубликован на Hetzner: `release_id=l115k-ac4eb8c6`, 244545299 байт, удержание 5.

Откат: `sudo python3 /home/ubuntu/transition-l115k.py rollback` → l115j (`133fe00c`).

Особенность этой посадки: впервые с 12.09 поехали настоящие миграции, 206 → 209 (`web651_source_text_chunk`, `web575_erasure_tombstone`, `web575_embed_share_audience`), расписка `prod/shared/l115k-migration/apply-receipt.json`, личность применения `web_migrator`.
2026-09-14T12:07:40.121Z · coordinator
[14.09 12:07Z координатор] WEB-573 — «Подключить Google Drive» не подключает
Обновление от 2026-09-14. Затронутая посадка: l115k (ac4eb8c68d70de829608bcae5a7acef693ab85fb, 13.09 17:27:44Z).
Карточка написана для человека, который открывает её впервые и не имеет ни журнала смены, ни переписки.

1. ЧТО БЫЛО СЛОМАНО

Пользователь нажимает «Подключить Google Drive». Диск не подключается. Продукт пишет «Не подключено» — без объяснения, что именно не получилось, и без следа в журнале, по которому можно было бы разобраться.

2. КАК НАШЛИ И ПОЧЕМУ НЕ ПОЙМАЛИ РАНЬШЕ

Первую причину нашла волна 3658 (`refs/waves/3658` = `7068c7b1`): настоящая причина была НЕ в цепочке подключения, а в ЧТЕНИИ СОХРАНЁННОГО СОСТОЯНИЯ. Упавший запрос статуса ПОКАЗЫВАЛСЯ КАК «НЕ ПОДКЛЮЧЕНО» и не оставлял следа в журнале. То есть продукт превращал «я не смог узнать» в «не подключено» — два разных факта с одним сообщением.
Почему не поймали раньше: сообщение об ошибке само было замаскировано под нормальное состояние.
Вторую, настоящую на сегодня причину нашёл СНИМОК ЭКРАНА ОТ ВЛАДЕЛЬЦА: при нажатии кнопки появлялось НАШЕ ЖЕ сообщение «Article 28 path closed». То есть путь закрывал НАШ СОБСТВЕННЫЙ заслон, а не Google.

3. ЭВОЛЮЦИЯ, ВКЛЮЧАЯ ТУПИКИ

3.1. Волна 3658 починила показ состояния и ПРЯМО ПРЕДУПРЕДИЛА в разделе известных проблем о заслоне статьи 28. Это предупреждение было пропущено, и второй круг начался с нуля.

3.2. ПРИЧИНА ЗАСЛОНА, найденная проверкой по дереву линии: реестр `src/content/legal/real/ARTICLE28_PROVIDER_CONFIRMATIONS.json` содержит ПУСТОЙ список подтверждений (`"confirmations": []`), поэтому заслон закрывает путь. Ничего не сломано — механизм работает как написан.

3.3. ТУПИК №1 — ключа подписи реестра не существовало вовсе.
Подпись записи реестра считается как код обмена с секретом (HMAC-SHA256), а САМ СЕКРЕТ В ПРОЕКТЕ ОТСУТСТВОВАЛ. Значит запись нельзя было ни подписать, ни проверить на рантайме. Ключ заведён координатором (64 знака, файл с правами 600) и в окружение прода должен идти вместе с посадкой — иначе приложение не проверит запись.

3.4. ТУПИК №2 — подобранный юридический документ оказался не тем.
Для записи в реестр был снят и захеширован документ: Cloud Data Processing Addendum, 903237 байт, sha256 начинается на `45734434`. Оговорка, из-за которой запись НЕ была внесена: этот документ применяется к клиентам Workspace и Cloud. Если Диск подключается ЛИЧНЫМ гугл-аккаунтом, применяются обычные условия использования и политика конфиденциальности — ДРУГОЙ документ. Подставить не тот означало бы записать в юридический реестр неправду. Отдельно отмечено, что отпечаток снят с HTML-страницы, которая меняется вёрсткой.

3.5. ОТВЕТ ВЛАДЕЛЬЦА И ПРИНЯТОЕ РЕШЕНИЕ. Владелец уточнил: Диск подключается ЛИЧНЫМ аккаунтом, Workspace нет, компании нет. Соглашения об обработке данных с Google у него НЕ СУЩЕСТВУЕТ.
РЕШЕНИЕ: ЗАПИСЬ В РЕЕСТР СТАТЬИ 28 НЕ ВНОСИТСЯ. Реестр — это реестр договоров с ОБРАБОТЧИКАМИ; написать «договор подтверждён» там, где договора нет, значит внести неправду в юридический реестр. Ключ подписи и отпечаток документа остаются заготовленными на случай, если разбор покажет, что запись нужна в другой форме.
ЭТО ТУПИК, В КОТОРЫЙ НЕ НАДО ХОДИТЬ СНОВА: соблазн «просто добавить запись, чтобы кнопка заработала» существует, и он отвергнут сознательно.

3.6. НАСТОЯЩИЙ ВОПРОС, ОТДАННЫЙ КОДУ. Подозрение для проверки: заслон, возможно, путает ДВА РАЗНЫХ ПОТОКА —
  (а) мы отправляем данные наших пользователей подрядчику на обработку ОТ СВОЕГО ИМЕНИ;
  (б) пользователь СВОИМ СОГЛАСИЕМ подключает СВОЁ хранилище, и мы читаем оттуда ЕГО ЖЕ файлы.
Во втором случае Google не наш субподрядчик, и реестр договоров тут ни при чём.
Волна 3726 (`refs/waves/3726` = `8bf5cc1d9`) получила задание: перечислить по коду, что и чьё уходит к Google, показать строку решения заслона, выбрать один из трёх исходов (освобождение с границей в коде / это поток обработчика и Диск остаётся закрытым / третий узкий вид записи), написать код и тесты с отрицательным контролем. Трогать реестр было запрещено явно.

4. ЧТО СДЕЛАЛИ В ИТОГЕ

Волна 3726 (`8bf5cc1d9`) влита в кандидат линии l115k; линия `ac4eb8c68d70de829608bcae5a7acef693ab85fb` посажена 13.09 17:27:44Z. Комментарий о посадке разослан в этот тикет.
Относящийся код в дереве линии: `src/lib/legal/article28Register.ts` (реестр и решение), `src/lib/integrations/featureToggleGate.ts` (место, где путь закрывается), реестр подтверждений — `src/content/legal/real/ARTICLE28_PROVIDER_CONFIRMATIONS.json`.

5. ЧЕМ ДОКАЗАНО И ГРАНИЦЫ ЗАЯВЛЕНИЯ

ДОКАЗАНО:
  Пост-QA линии l115k (волна 3777): проверка `googleServerAuthArticle28` бьёт НАСТОЯЩУЮ ТОЧКУ ВХОДА с отрицательными контролями, 5 из 5. Это не проверка по описанию, а прогон реального пути.

ГРАНИЦЫ — читать обязательно:
  Доказано поведение ГРАНИЦЫ В КОДЕ. НЕ ДОКАЗАНО, что живое нажатие пользователем на проде теперь действительно подключает Диск: никто не входил в Google из браузера.
  Запись в реестр статьи 28 НЕ ВНЕСЕНА и вноситься не должна в нынешнем виде.

6. ЧТО ОСТАЛОСЬ ОТКРЫТЫМ

  Живая попытка подключения Диска человеком в браузере.
  Прямое следствие, тянущее за собой другой тикет: 110 документов, у которых мы НИГДЕ НЕ ХРАНИМ САМИ БАЙТЫ, возвращаются только ПЕРЕКАЧКОЙ из Drive — а перекачка идёт через этот же путь. Подробности в WEB-593.

7. TROUBLESHOOTER — ЕСЛИ «НЕ ПОДКЛЮЧЕНО» ПОЯВИТСЯ СНОВА

Шаг 1. Посмотреть ОТВЕТ СЕРВЕРА И КОНСОЛЬ БРАУЗЕРА на наш собственный текст: `article28.path.closed` или `ARTICLE28_CONTRACT_REQUIRED`. Если он там есть — путь закрыл НАШ ЗАСЛОН, а не Google. Дальше идти по шагу 2.

Шаг 2. Открыть `src/content/legal/real/ARTICLE28_PROVIDER_CONFIRMATIONS.json`. Пустой список `confirmations` закрывает путь — это устройство механизма.

Шаг 3. НЕ «ЧИНИТЬ» ЭТО ЗАПИСЬЮ В РЕЕСТР. Для ЛИЧНОГО гугл-аккаунта договора обработки данных не существует, и запись была бы неправдой в юридическом реестре. Решение уже принималось и подтверждено владельцем. Если кажется, что запись всё же нужна, — это вопрос о ФОРМЕ записи и о том, какой из двух потоков перед вами (мы отправляем чужие данные подрядчику / пользователь читает свои файлы из своего хранилища), а не вопрос «добавить строку».

Шаг 4. Если в ответе НЕТ текста про статью 28, а «Не подключено» всё равно показывается — это второй, более старый сюжет: упавший ЗАПРОС СТАТУСА показывается как «не подключено». Искать след запроса статуса в журнале; исторически его там не было вовсе, и именно это чинила волна 3658.

Шаг 5. Если понадобится подписывать запись реестра — секрет подписи HMAC-SHA256 в проекте ИСТОРИЧЕСКИ ОТСУТСТВОВАЛ и был заведён отдельно. Без него приложение не проверит запись на рантайме, и подписанная где-то запись просто не пройдёт.
2026-09-14T14:06:11.112Z · coordinator
[14.09 14:06Z координатор] VERDICT=GO

# WEB-573: подключение Google Drive молча возвращает «Не подключено»

Волна 3860 (до-закрытие карточки WEB-573, база `refs/waves/l115n`, ветка `wave/3860-web573-drive-not-connected`, HEAD `d85bb2dad`).
Роль волны: не чинить заново, а доказать, что диагноз волны 3658 верен **и сейчас**, что фикс держится, и что у карточки есть отрицательный контроль (тест, который краснеет на коде до фикса).

---

## Для тикета — полный след детским языком (для нулевого агента)

### 1. Симптом словами пользователя

Человек уже подключал Google Drive (кнопка «Подключить» отработала, токен сохранился на сервере). Он снова открывает окно «Интеграции» — и видит «Не подключено», как будто связи никогда не было. Никакой ошибки, никакого сообщения, и в логах ничего. При этом на самом деле связь есть.

### 2. Как нашли и почему не поймали раньше

Причина оказалась **не в самой цепочке подключения** (OAuth-обмен, сохранение токена), а в **чтении сохранённого состояния** при открытии окна. Это отдельный, последний неохраняемый участок пути.

Раньше всё работало так (код **до** фикса, коммит `7068c7b1f^`):

- Окно интеграций при открытии спрашивает сервер: «есть ли у меня сохранённое подключение?» — это `loadOAuthIntegrationsFromServer()` → `GET /api/integrations/oauth/result`.
- Старый код функции любой сбой (сервер ответил ошибкой, сеть отвалилась, ответ пришёл кривой) превращал в одно и то же `null` (файл `src/lib/auth/oauthIntegrationPersistence.ts`, строка вида `if (!response.ok || !body) return null;`).
- Окно на `null` просто **ничего не делало** — ни надписи, ни записи в лог (`src/components/modals/IntegrationsModal.tsx`: `.then((projection) => { if (projection) hydrate(...); })`).
- Метка «Подключено / Не подключено» рисуется из того, что вернуло чтение. Раз вернулось «пусто» — рисуется «Не подключено». Молча.

Почему не поймали раньше: все предыдущие волны чинили **цепочку подключения** — отмену согласия, неполный доступ к Drive, локализацию текста ошибки, сбой обмена токена (коммиты `4cf7bfbe6`, `dfa9e2333`, `9aaeaee30`, `d5c1658bd`, `07b7fab7a`, `aaad22f13` и др. — все с меткой WEB-573). Каждый из них чинил реальную, но **другую** поломку. А само чтение состояния при открытии окна не проверял никто: сбой там редкий и «проходящий» (короткий блин базы или обрыв сети), проявляется только иногда, и когда проявляется — выглядит как «не подключено» без единого следа, который можно было бы сопоставить с жалобой.

### 3. Что сделали (фикс волны 3658, коммит `7068c7b1f` от 2026-09-13, уже в этой ветке)

Теперь «действительно не подключено» и «не смогли узнать» — **разные вещи**, и сбой чтения оставляет след:

1. **Сервер** `src/app/api/integrations/oauth/result/route.ts:52-61` — чтение из базы обёрнуто в `try/catch`. Раньше ошибка базы просто падала «мимо». Теперь: `console.error('[oauth-result] failed to read OAuth client account state', error)` + ответ `500` с явной причиной `reason: "read-failed"`. Честное «никогда не подключались с этого браузера» остаётся `409` с `reason: "not-bound"`.
2. **Клиент** `src/lib/auth/oauthIntegrationPersistence.ts:110-151` — `loadOAuthIntegrationsFromServer()` теперь возвращает различимый результат: `{ok:true, …}` | `{ok:false, reason:"not-bound"}` | `{ok:false, reason:"request-failed", …}`. Любой сбой пишется в `console.error` (три разных места), а не молчит.
3. **Окно** `src/components/modals/IntegrationsModal.tsx:183-216` — `not-bound` (честное «никогда») → молча, как и раньше; любой другой сбой → `console.error` **и** всплывающее сообщение «Could not verify your connection status». Человек видит разницу.

### 4. Чем доказано (офлайн, своими руками)

- **Фикс в коде и жив**: `7068c7b1f` — предок текущего HEAD (`git merge-base --is-ancestor` = да), и после фикса эти три файла никто не менял (`git log 7068c7b1f..HEAD` по ним пустой). Доказательство: `evidence/git-ancestry.txt`.
- **Юнит-тест чтения проходит на сегодняшнем коде**: `src/lib/auth/__tests__/oauthIntegrationStatusRead.test.ts` — **6 из 6 зелёные** (в т.ч. «500 → request-failed, а не not-bound», «сеть отвалилась → request-failed», «кривой JSON → request-failed»). Запуск без node_modules через штатный резолвер репозитория `node --import ./scripts/ts-node-resolver.mjs --test …`. Доказательство: `evidence/green-oauthIntegrationStatusRead.txt`.
- **Отрицательный контроль краснеет на коде до фикса**: тот же самый файл теста, запущенный против **старого** `oauthIntegrationPersistence.ts` (взят из `7068c7b1f^`), даёт **6 из 6 красных** — все проверки падают, потому что старая функция возвращает `null` вместо различимого результата. Доказательство: `evidence/red-negative-control.txt` (внутри видно `actual: null` против ожидаемого `{ok:false, reason:"request-failed", …}`). Именно этого и требовала карточка: тест обязан краснеть на коде до фикса — и он краснеет.
- **След в логе есть**: в выводе зелёного прогона видны сами строки `[oauth] could not reach …` / `[oauth] failed to load durable OAuth connection state …` — то есть упавший запрос теперь оставляет след. Дополнительно серверный путь покрыт тестом `src/app/api/integrations/oauth/result/__tests__/routeGet.test.ts` (3 случая: успех / честный not-bound / «БД упала → 500 read-failed, без утечки текста ошибки»).
- **Соседние тесты волны 3658 проходят**: `web573OAuthRecovery.test.ts` + `oauthAtomicWiring.test.ts` — **8 из 8 зелёные** (`evidence/green-web573OAuthRecovery+wiring.txt`).

### 5. Где ГРАНИЦА (что доказано, а что — только заявлено)

**Доказано офлайн** (код + тесты): сама механика — старый код сводил любой сбой чтения к `null` и молча рисовал «Не подключено»; новый код различает `not-bound` / `request-failed`, логирует сбой и показывает человеку сообщение; тест-отрицательный-контроль краснеет на старом коде и зеленеет на новом.

**НЕ доказано офлайн, потому что нет `node_modules`** (`next`, `zod`): серверный тест `routeGet.test.ts` я **не запускал** — он требует `next/server` и `zod`, которых в этой копии репозитория нет. Его корректность подтверждена только чтением кода и наличием самого теста, не выполнением. Точно так же я не рендерил само окно в браузере — поведение UI доказано чтением кода, не картинкой на экране.

**Что нужно, чтобы закрыть карточку на 100%** — один короткий сценарий человеком в браузере (ниже). Офлайн-доказательства достаточно, чтобы **закрыть дефект по существу**; сценарий в браузере нужен, если хочется end-to-end подтверждения «глазами», а не только кодом.

### 6. Сценарий для человека в браузере (по шагам, если требуется визуальное подтверждение)

1. Открыть приложение, подключить Google Drive штатной кнопкой (получить сохранённое подключение). Убедиться: метка «Подключено к Google».
2. В DevTools → Network включить «Offline» (или заблокировать/уронить `GET /api/integrations/oauth/result` в Network → Request blocking).
3. Переоткрыть окно «Интеграции».
4. **Ожидание (после фикса)**: появляется всплывающее сообщение «Could not verify your connection status» (и в консоли `[integrations] failed to load durable OAuth integrations`). Метка может оставаться «Не подключено» — это нормальное состояние «не знаем», но теперь оно **озвучено**.
5. Контроль «действительно отключено»: на свежем браузере/профиле без подключения открыть окно → никакого сообщения, просто «Не подключено» (честное «никогда не подключались»).
6. Контроль «сервер увидел»: в логах сервера при уроне БД — строка `[oauth-result] failed to read OAuth client account state`.

---

## Что сделано волной (сводка ссылок на файл и строку)

- Прочитан и воспроизведён весь путь чтения состояния: `src/components/modals/IntegrationsModal.tsx:183-216` → `src/lib/auth/oauthIntegrationPersistence.ts:110-151` → `src/app/api/integrations/oauth/result/route.ts:46-67` → `src/lib/auth/oauthAccountState.ts:125-145` → метка `IntegrationsModal.tsx:1352-1395` (рендер «Подключено/Не подключено»).
- Установлено, что симптом **в сегодняшнем коде закрыт** (фикс `7068c7b1f` является предком HEAD и не отменён).
- Подтверждено разделение состояний: `not-bound` (честно «нет») против `request-failed` (не смогли узнать) — и в типе результата, и в UI (toast).
- Подтверждено наличие следа в журнале у упавшего запроса (серверный `console.error` + клиентский `console.error` + toast).
- Запущены тесты: 6/6 (чтение), 8/8 (recovery+wiring), и **отрицательный контроль 6/6 красных на до-фикс коде**.
- Собраны доказательства в `evidence/` с `SHA256SUMS`.

## Отрицательный контроль — как именно ставился

Тот же файл теста `src/lib/auth/__tests__/oauthIntegrationStatusRead.test.ts` (не менялся) запущен дважды:
- против сегодняшнего `oauthIntegrationPersistence.ts` → **зелёный** (6/6);
- против старой версии модуля из `7068c7b1f^:src/lib/auth/oauthIntegrationPersistence.ts` → **красный** (6/6 fail, `actual: null`).

Оба прогона — через штатный `scripts/ts-node-resolver.mjs`, без node_modules. Команды и результаты в `evidence/run-exitcodes.txt` и файлах `green-*` / `red-*`.

---

## KNOWN ISSUES

1. **Нелокализованный текст ошибки чтения.** Ключ перевода `integrations.status.checkFailed`, который использует окно в тосте (`IntegrationsModal.tsx:200,207`), **отсутствует** во всех словарях — в `src/lib/translations.ts` есть только `integrations.status.connected` и `integrations.status.disconnected` (строки 11517-11518), ключа `checkFailed` нет (подтверждено `grep -c` = 0). Итог: всплывающее сообщение всегда на английском («Could not verify your connection status»), даже в русском интерфейсе. Дефект не молчит — сообщение видно, но оно не локализовано. Минор.
2. **Чтение состояния не пишет метрику.** `GET /api/integrations/oauth/result` не вызывает `recordApiMetric` (в отличие от `POST` в том же файле, `route.ts:81,127`). Сбой чтения виден в stdout/stderr-логах (`console.error`), но не попадёт в дашборд API-метрик. Минор, отдельный вопрос наблюдаемости.
3. **Метка осталась бинарной.** При сбое чтения метка по-прежнему показывает «Не подключено» (просто теперь рядом появляется предупреждение). Настоящего третьего состояния «подключение неизвестно/проверяем» в UI нет. Это осознанная граница фикса: требование «человек должен видеть разное» выполнено через toast, а не через трёхзначную метку.
4. **Серверный тест не запущен офлайн.** `routeGet.test.ts` требует `next`/`zod` из node_modules (отсутствуют в этой копии); запустить его здесь нельзя — см. «Где ГРАНИЦА».

## Troubleshooter (нулевой агент, куда смотреть)

- «Вижу Не подключено, но человек говорит что подключал» → это и есть WEB-573. Проверь, что открылся тост «Could not verify your connection status»; если тоста нет и нет `[integrations] failed to load durable OAuth integrations` в консоли браузера — регресс фикса, смотри `IntegrationsModal.tsx:183-216`.
- «Проверить, что фикс на месте» → `git merge-base --is-ancestor 7068c7b1f HEAD` и пустой `git log 7068c7b1f..HEAD -- src/lib/auth/oauthIntegrationPersistence.ts src/app/api/integrations/oauth/result/route.ts src/components/modals/IntegrationsModal.tsx`.
- «Быстро прогнать тест чтения без node_modules» → `node --import ./scripts/ts-node-resolver.mjs --test src/lib/auth/__tests__/oauthIntegrationStatusRead.test.ts` (ожидание: 6/6).
- «Прогнать отрицательный контроль» → повторить скрипт из `evidence/` (суть: тот же тест против `7068c7b1f^:…oauthIntegrationPersistence.ts`), ожидание: 6/6 красных.
- «Лог упавшего запроса» → сервер: `[oauth-result] failed to read OAuth client account state`; клиент: `[oauth] could not reach …` / `[oauth] failed to load durable OAuth connection state …`.

## Итог

Симптом, описанный в карточке, **в сегодняшнем коде закрыт**: чтение сохранённого состояния больше не сводит сбой к молчаливому «Не подключено», состояния «действительно отключено» и «не смогли узнать» различимы, упавший запрос оставляет след в логе, и на месте стоит отрицательный контроль, который краснеет на до-фикс коде. Дефект закрыт по существу офлайн; для стопроцентного end-to-end подтверждения достаточно 5-минутного сценария в браузере (приведён выше). Оставшиеся пункты — локализация тоста, метрика на GET и бинарная метка — минорные, карточку не блокируют.
2026-09-14T15:27:56.744Z · coordinator
[14.09 15:27Z координатор] VERDICT=NO-GO

# WEB-573: независимая приёмка волны 3860

## Коротко, детским языком

Я взял ровно ветку автора `refs/waves/3860/wave/3860-web573-drive-not-connected`.
Она существует. Её текущий commit я получил командой git: `d85bb2dad15a3ba52c773b0a2362748009a2c3b9`.
Отдельное дерево для приёмки создано в `wt-3873-acc-web573` на ветке
`acceptance/3873-acc-web573`.

Я проверил две разные вещи:

1. Когда сервер не может прочитать сохранённое состояние, это должно быть
   ошибкой проверки состояния, а не надписью «Не подключено».
2. Когда человек отменил Google OAuth, дал не тот доступ или Google вернул
   ошибку, приложение не должно сохранять плохой результат как подключение.

Некоторые хорошие случаи устояли: локальные проверки callback, UI и чтения
статуса прошли. Но отрицательная проверка нашла настоящий контрпример:
успешный HTTP-ответ без поля `integrations` принимается как честный пустой
список подключений. Это malformed body, а не доказанное «нет подключений».
Такой ответ может убрать ранее известное подключение из UI. Поэтому работа не
может считаться релиз-кандидатом.

## Что было заявлено и что я доказал сам

Заявления из локального ticket trail не принимались на слово. В частности,
заявленный там результат пост-QA не использовался как мой результат.

Самостоятельно полученные результаты:

- `node --test scripts/__tests__/web573-independent-acceptance.test.mjs`:
  тесты callback/consumer/static guards — 5 passed, 0 failed.
- `node --import .../tsx/dist/loader.mjs --test` для чистых TS-тестов
  consent, Google IDs, локализации, чтения статуса и OAuth recovery:
  20 passed, 0 failed.
- route-тесты `GET /api/integrations/oauth/result`:
  connected, never-bound и DB read failure — 3 passed, 0 failed.
- route-тесты `GET /api/integrations/google/auth` exchange outcomes:
  success, provider failure и replay — 3 passed, 0 failed.
- route-пакет persistence/origin/budget/metrics, read route и удаление
  service-account shortcut: 11 passed, 0 failed.

В этих запусках я не запускал production, прод-БД, SSH, deployment, release
build, `next build` или полный `tsc`.

## Отрицательный контроль, который сломал вывод

Я добавил независимый тест
`scripts/__tests__/web573-acceptance-negative.test.mjs` и запустил его через
`node --test` с уже имеющимся локальным TypeScript loader.

Условие контроля:

```text
HTTP status = 200
body = { generation: "negative-control" }
```

Ожидание: `ok:false`, `reason:"request-failed"`, потому что обязательного
массива `integrations` нет.

Получено на самом деле:

```text
{ ok: true, generation: "negative-control", integrations: [] }
```

Тест завершился с `RC=1` именно на этом сравнении. Причина видна в коде:
`src/lib/auth/oauthIntegrationPersistence.ts` превращает отсутствующий или
не-массивный `body.integrations` в `[]`, а затем принимает ответ, если есть
только непустой `generation`. Валидации формы projection целиком нет.

Это не теоретическое чтение кода: условие было подано функции, и функция
вернула неправильный для заявленного контракта результат.

## Что устояло

- Ошибка чтения состояния `500` возвращается как `reason:"read-failed"`, а
  не как `not-bound`; текст внутренней DB-ошибки наружу не выходит.
- Сетевой сбой, не-JSON тело и ответ `200` без `generation` различаются от
  честного `not-bound`.
- Успешный callback проверяет Drive scope до `setCredentials` и до payload
  `success:true`.
- Ошибка обмена токена получает объяснение и записывается с reason code.
- Повторно заявленный callback не доходит до обмена токена.
- Ошибки отмены/неполного scope не попадают в durable persistence.
- Проверки прокси не пропускают неправильный origin для persistence write.

## Что не устояло

1. Контрпример malformed projection: отсутствующий `integrations` при `200`
   принят как `{ok:true, integrations:[]}`. Нужно валидировать, что
   `integrations` существует и является массивом, а элементы имеют допустимую
   форму; при нарушении возвращать `request-failed`.
2. Тест автора `src/components/modals/__tests__/googleConsentIndependentQa.test.mjs`
   не воспроизводится в текущем дереве: после запуска с локальными
   зависимостями он падает на ожидаемом типе toast. Его mock возвращает старый
   `null` из `loadOAuthIntegrationsFromServer()`, тогда как текущий consumer
   читает `result.ok`; фоновый `TypeError` добавляет `error` toast и ломает
   ожидание `info` для `user_cancelled`. Это дефект evidence harness.
3. В самой ветке автора не найден отдельный авторский report/evidence-файл;
   `git ls-tree` показал исходники и тесты WEB-573, но не сдачу с отчётом.

## Граница проверки

Я проверял код и локальные тестовые сценарии в acceptance worktree. Google
аккаунт в браузере не подключал и production не трогал. Поэтому я не доказал
живое нажатие «Подключить Google Drive» с реальным Google consent и не могу
утверждать, что реальный пользователь сейчас подключается.

Прямая доска с телом тикета и комментариями отсюда не видна; доступен только
локальный trail `/Users/limamarty/waves/3848-out/WEB-573.txt` и его копия
`/Users/limamarty/waves/3848TRAILS-evidence/WEB-573.txt`. Их формулировки я
считал заявками, а не результатами этой приёмки.

## Что нужно для повторной приёмки

1. Исправить loader: при `200` отклонять отсутствие `integrations`, не-массив
   и недопустимые элементы как `request-failed`; сохранить отдельный
   `not-bound` только для серверного `409 reason:not-bound`.
2. Обновить independent consumer mock так, чтобы он возвращал объект нового
   discriminated contract, затем повторить тест через `node --test`.
3. Повторить отрицательный контроль: подать `200` без `integrations` и
   убедиться, что функция возвращает `ok:false/reason:request-failed`.
4. Для живого пользовательского утверждения нужен безопасный non-production
   браузерный сценарий: открыть локальный app, войти тестовым аккаунтом,
   нажать Connect, отменить consent, проверить объяснение; затем повторить с
   Drive scope и проверить успешное подключение и сохранённое состояние.

## KNOWN ISSUES

- В acceptance worktree отсутствует `node_modules`; зависимости не
  устанавливались. Для воспроизводимых TS/route запусков использована только
  уже существующая локальная копия зависимостей из другого worktree через
  `NODE_PATH` и read-only alias loader. Это ограничение среды, не production.
- В системе нет команды `timeout` или `gtimeout`; первая требуемая команда с
  `timeout 120 ...` закончилась `RC=127` до запуска теста. Остальные тестовые
  команды были короткими и выполнены синхронно.
- Не запускались production, прод-БД, SSH, sudo, secrets, платные провайдеры,
  deployment, release build, `next build`, полный `tsc`, чужие worktree или
  процессы.
2026-09-14T15:51:16.635Z · coordinator
[14.09 15:51Z координатор] VERDICT=GO

# WEB-573 (приёмка 3873): успешный ответ без поля `integrations` не должен считаться «подключений нет»

Волна 3880. Чиню находку независимой приёмки 3873, не переоткрывая саму карточку WEB-573.
База `refs/waves/l115n` = `d85bb2dad`; ветка `wave/3880-web573-malformed-body`; коммит фикса `c4aa8e4ce`.

Находка 3873: «Успешный HTTP-ответ **без поля `integrations`** принимается как честный пустой список подключений. Это malformed body, а не доказанное "подключений нет".»

---

## Для тикета — полный след детским языком (для нулевого агента)

### 1. Симптом словами пользователя

Человек подключил Google Drive (кнопка сработала, токен лежит на сервере). Он перезагружает страницу, открывает окно «Интеграции» — и видит «Не подключено», как будто связи никогда не было. Ни ошибки, ни сообщения, ничего. Связь при этом есть.

Это **тот же** симптом, что был в WEB-573, но через **другую дырку**: не «запрос упал», а «запрос ответил 200, но в теле нет поля `integrations`».

### 2. Как нашли и почему не поймали раньше

Читаю путь: окно → `loadOAuthIntegrationsFromServer()` (`src/lib/auth/oauthIntegrationPersistence.ts`) → `GET /api/integrations/oauth/result`.

Разбор ответа (старый код, `oauthIntegrationPersistence.ts:141-150`):

```ts
const generation = typeof body.generation === 'string' ? body.generation.trim() : '';
const integrations = Array.isArray(body.integrations)
    ? body.integrations.filter(...)
    : [];                                   // ← ВОТ ОНО: нет поля → пустой список
if (!generation) { ... request-failed ... }
return { ok: true, generation, integrations };  // ← пустой список выдаётся как честная правда
```

Строка `: []` делает отсутствующее поле неотличимым от честного пустого списка. А `{ ok: true }` дальше отдаётся окну как «авторитетная проекция»: `IntegrationsModal.tsx:189` вызывает `hydrateConfirmedOAuthIntegrations(result.integrations, ...)`. Пустой список → реальное подключение не показывается → «Не подключено».

**Почему не поймали раньше.** Волна 3860 закрыла чтение от сбоев (сеть, не-2xx, кривой JSON, отсутствие `generation`), но оставила именно этот случай. Её отрицательный тест (`oauthIntegrationStatusRead.test.ts`) проверял «200 без `generation`» (тело `{ integrations: [] }`), а «200 с `generation`, но без `integrations`» — нет. Дыра в форме тела, которой в тесте не было.

### 3. Эволюция с тупиками

- **Шаг 1.** Подтвердил находку прогоном теста приёмки на старом коде: красный, `actual: { ok: true, generation: 'negative-control', integrations: [] }` (`evidence/red-acceptance-negative.txt`). Дырка реальна.
- **Тупик 1 — дать состоянию «поля нет» отдельный `reason` (например `malformed-body`).** Тест приёмки жёстко зашит через `assert.deepEqual(result, { ok:false, reason:"request-failed", status:200 })` — точное совпадение всех ключей. Отдельный reason его провалит. Значит «поля нет» обязано возвращать `reason: "request-failed"` с `status: 200`, без лишних ключей (даже `message: undefined` ломает deepEqual — проверил). Отказ от отдельного reason.
- **Тупик 2 — чинить в окне/`hydrate`.** Окно с волны 3860 уже правильно ветвится: `result.ok` → hydrate, иначе (`not-bound` → тихо; любой другой `ok:false` → тост, без hydrate) — `IntegrationsModal.tsx:186-216`. Дыра не в потребителе, а в том, что парсер выдаёт `ok:true` на кривое тело. Значит чиню в источнике, а не в потребителе.
- **Уточнение про «стирает подключение».** `hydrateConfirmedOAuthIntegrations` (`useStore.ts:4698-4712`) не заменяет список, а сливает по `type` (никогда не удаляет). Точная формулировка симптома: подключение, известное только серверу, **не показывается** (рисуется «Не подключено»), а не «выдёргивается из уже нарисованного списка». Для пользователя разницы нет — он видит «Не подключено» при живой связи.
- **Шаг 4.** Чиню только парсер (одно место — единственный источник «что считать честным пустым»).

### 4. Что сделали (со ссылками на файл и строку)

Файл `src/lib/auth/oauthIntegrationPersistence.ts`, функция `loadOAuthIntegrationsFromServer()`:

- `:141` — `generation` теперь читается и проверяется **раньше** списка.
- `:153-156` — **новое**: `if (!Array.isArray(body.integrations))` → `console.error(... "missing the integrations field" ...)` и возврат `{ ok:false, reason:'request-failed', status: response.status, ...(message ? { message } : {}) }`. Отсутствующее или не-массивное поле = ошибка формы, а не пустой список.
- `:157-159` — `integrations` фильтруется и попадает в `{ ok:true }` только когда это настоящий массив (пустой — честно, непустой — как раньше).

Разделяются три состояния (подробно в разделе «Три состояния»):

| Состояние | Возврат | Действие окна |
|---|---|---|
| поле есть и пусто (`integrations: []`) | `{ ok:true, generation, integrations:[] }` | hydrate (честно «нет») |
| поля нет / не массив | `{ ok:false, reason:'request-failed', status:200 }` | тост, **без** hydrate |
| запрос не удался (сеть / не-2xx / не-JSON) | `{ ok:false, reason:'request-failed' }` | тост, **без** hydrate |

Второе и третье не стирают ранее известное подключение: окно не зовёт hydrate ни при одном `ok:false`, кроме честного `not-bound`.

Тесты:
- `src/lib/auth/__tests__/oauthIntegrationMalformedBody.test.ts` — отрицательный контроль, 5 случаев (нет поля → request-failed; поле пустое → ok:true; поле не массив → request-failed; мусор внутри массива отбрасывается; 500 → request-failed).
- `scripts/__tests__/web573-acceptance-negative.test.mjs` — дословный тест приёмки 3873, перенесён в дерево и закреплён (без машинно-специфичного `.tmp/…-loader.mjs` из ветки приёмки).

### 5. Чем доказано и ГДЕ ГРАНИЦА

**Доказано офлайн (своими руками):**
- Старый код сворачивает отсутствующее поле в пустой список: красный прогон с `actual: { ok: true, ..., integrations: [] }` — `evidence/red-acceptance-negative.txt`, `evidence/red-malformed-body.txt` (оба RC=1).
- Новый код различает: мой тест 5/5 зелёных, тест приёмки 1/1 зелёный, старый тест чтения 6/6 зелёных (регресса нет) — `evidence/green-*.txt`.
- Фикс в коммите `c4aa8e4ce`, бандл `3880WEB573FIX.bundle` прошёл `git bundle verify` (требуемая база `d85bb2dad`, tip `c4aa8e4ce`), sha256 в `3880WEB573FIX.bundle.sha256`.

**ГРАНИЦА (что не доказано):**
- **Реальный сервер сегодня всегда шлёт `integrations`.** `route.ts:63-66` возвращает `Object.values(state.integrations)`, а `state.integrations` гарантированно объект (`oauthAccountState.ts:41-42,76-77`: `normalizeObject` падает в `{}`). То есть форму «200 без `integrations`» **текущий** сервер произвести не может — фикс это защита от регрессии сервера / прокси, который вырежет поле / рассинхрона версий. Поэтому тест и мокает тело напрямую — это правильный уровень (юнит).
- Серверный тест `routeGet.test.ts` не запускал (нужны `next`/`zod` из node_modules, их нет) — та же граница, что волна 3860.
- Окно в браузере не рендерил: поведение тоста/ветвления доказано чтением кода, не скриншотом.

### 6. Три состояния — что видит пользователь

- **«Поле есть и пусто» (честно нет).** `ok:true` → hydrate. Никакого тоста. Рисуется «Не подключено» — и это правда.
- **«Поля нет» (ошибка формы).** `ok:false` → тост **«Could not verify your connection status»** + подпись **«Your existing connections were not affected. Reload and try again.»**. Список подключений не трогается. Метка может по-прежнему показывать «Не подключено» (она бинарная — известный пункт волны 3860), но теперь рядом честное «мы не смогли проверить», а не молчаливая ложь.
- **«Запрос не удался».** Тот же тост, что и в «поля нет».

**Не станет ли «ошибка формы» новой непонятной надписью?** Нет. Состояние «поля нет» намеренно повторно использует **уже существующий** тост волны 3860 для любого `request-failed` (`IntegrationsModal.tsx:200`). Новой строки не добавляется. Единственный известный огрех этого тоста — он не локализован (см. KNOWN ISSUES №1), но это унаследовано от 3860, а не внесено здесь.

### 7. Где ЕЩЁ работает этот механизм (обход дерева)

Класс — «отсутствующее поле ответа принято за пустое значение». Обошёл `src/lib`, `src/app`, `src/store` по паттернам `Array.isArray(x) ? x : []`, `x ?? []`, `x || []` и разбору `.json()`. Опасная разновидность класса (пустой список становится авторитетным и стирает ранее известное состояние) — **ровно одна**, та что починена:

- `src/lib/auth/oauthIntegrationPersistence.ts:142-145` → **починено** (см. выше).

Остальные вхождения — не тот класс:

- **Уже защищены (не-массив → выход/`null`, а не пустота):** `useStore.ts:3598` (`syncAgentsFromServer`: `if (!Array.isArray(serverAgents)) return;`), `useStore.ts:3617` (`syncUserAgents`), `useStore.ts:1267-1268` (`sanitizeIntegrationMirror` возвращает `null`), `useStore.ts:1309`/`loadIntegrationMirror` (возвращает `null`), `factcheck/inlinePersistence.ts:164` (`!Array.isArray(data.checks) return null`), `oauthIntegrationPersistence.ts:79-99` (`persistOAuthIntegrationOnServer`, путь записи, проверяет поля явно).
- **Эфемерные свежие результаты, где «пусто» — законное «ничего не нашлось», без стирания долговременного состояния:** провайдеры внешних служб `factcheck/providers/*.ts` (`data.items ?? []`, `data.results ?? []`, `data.claims ?? []` — внешние API поиска/источников), `factcheck/runResponse.ts:148` (хелпер `stringArray` для необязательных полей; причём `runResponse.ts:352-355` уже различает `claims !== undefined` с кривым типом — правильный образец), `research/selfCheck.ts:176`, `factcheck/verdictGuard.ts:588`, `packager.ts:413` (`fetchDecksForPack` — чтение для экспорта, пусто = «колод нет»), `shop/purchaseFlow.ts:29` (`fetchActiveBoosts` — пусто = «активных бустов нет»).

Вывод: починка одного места здесь и есть полная починка — других мест с опасной семантикой «отсутствующее поле → пустой список → стирание известного подключения» в дереве нет.

---

## KNOWN ISSUES

1. **Тост не локализован (унаследовано от 3860, не внесено).** Ключ `integrations.status.checkFailed` отсутствует в словарях — тост всегда английский «Could not verify your connection status». След волны 3860, не блокирует.
2. **Метка осталась бинарной.** При «ошибке формы»/«сбое запроса» метка по-прежнему может показывать «Не подключено», лишь рядом тост «не смогли проверить». Настоящего третьего визуального состояния «неизвестно» нет — осознанная граница (та же, что в 3860).
3. **Чтение не пишет метрику.** `GET /api/integrations/oauth/result` не вызывает `recordApiMetric` (в отличие от `POST` в том же файле). Сбой виден в логах (`console.error`), но не в дашборде. Отдельный вопрос наблюдаемости, вне рамок этой находки.
4. **Состояние 2 и 3 делят `reason: 'request-failed'`.** Это вынужденно: тест приёмки жёстко требует именно `reason:"request-failed"` для «поля нет». Различаются они `status` (200 vs ошибка/отсутствие) и текстом в логе. Функционально это верно (оба = «не смогли узнать, не стираем»), но если в будущем захочется отдельного reason — придётся сначала поменять тест приёмки.
5. **`!generation`-ветка держит `message: undefined` как ключ.** `oauthIntegrationPersistence.ts:142-144` (старый код) возвращает `{..., message}` с `message` = `undefined`. Безвредно (ни один тест не сверяет её точную форму), но несимметрично с новой веткой, которая ключ опускает. Не трогал, чтобы не менять больше нужного.

## Troubleshooter (нулевой агент, куда смотреть)

- «Вижу "Не подключено", но человек говорит что подключал, при этом тоста "Could not verify" нет» → проверь ответ `GET /api/integrations/oauth/result`: если 200 и в теле есть `generation`, но нет массива `integrations` — это ровно эта находка. Смотри `oauthIntegrationPersistence.ts:153-156`.
- «Проверить, что фикс на месте» → `git merge-base --is-ancestor c4aa8e4ce HEAD` и наличие `if (!Array.isArray(body.integrations))` в `oauthIntegrationPersistence.ts`.
- «Быстро прогнать отрицательный контроль без node_modules» → `node --import ./scripts/ts-node-resolver.mjs --test src/lib/auth/__tests__/oauthIntegrationMalformedBody.test.ts` (ожидание 5/5 зелёных) и `… --test scripts/__tests__/web573-acceptance-negative.test.mjs` (1/1).
- «Прогнать красный на старом коде» → подставить старую `oauthIntegrationPersistence.ts` из `c4aa8e4ce^` и повторить тест: ожидание `actual: { ok:true, …, integrations: [] }`.
- «Лог кривого тела» → клиент: `[oauth] OAuth connection state response was missing the integrations field`; окно: `[integrations] failed to load durable OAuth integrations`.

## Итог

Находка 3873 подтверждена и починена в источнике: отсутствующее поле `integrations` в успешном ответе больше не сворачивается в честный пустой список, а возвращается как `request-failed` (ошибка формы), которую окно озвучивает тостом и не стирает известное подключение. Отрицательный контроль краснеет на старом коде и зеленеет на новом (оба прогона в evidence). Других мест с опасной семантикой «отсутствующее поле → пусто → стирание известного состояния» в дереве нет. Сдано: отчёт + бандл (`git bundle verify` ок) + evidence с SHA256SUMS.
2026-09-14T16:05:46.824Z · coordinator
[14.09 16:05Z координатор] VERDICT=GO

# Независимая приёмка 3886 / WEB-573

## Для тикета — детским языком

Я проверял, что серверный ответ про подключённые сервисы не врёт, когда в нём нет нужного поля.

Проверял так:

- взял ветку автора и отдельную ветку старой приёмки;
- прогнал тест приёмки 3873 на новом коде;
- написал отдельный тест и прогнал его на новом и старом коде;
- отдельно прочитал все места, где ответ про OAuth-подключения разбирается и попадает в экран.

Что устояло:

- Поле `integrations` есть и равно `[]`: это честный ответ «подключений нет», loader возвращает `ok: true`.
- Поля `integrations` нет: это ошибка формы, loader возвращает `ok: false, reason: "request-failed", status: 200`.
- Запрос не удался: loader возвращает `ok: false, reason: "request-failed"`; пустой список не создаётся.
- В окне hydrate вызывается только внутри ветки `result.ok`. При ошибке поле не подставляется и уже известное подключение не заменяется пустым списком; вместо этого показывается сообщение «не удалось проверить состояние».

Граница проверки: я доказал локальный loader, его потребителя чтением кода и mocked HTTP-ответы. Браузерный рендер окна не запускал. Реальный сервер и БД не трогал: это запрещено условиями волны. Тест route GET не исполнился, потому что в worktree нет пакета `tsx`; ниже указано, что для этого нужно.

Открыто: для полной проверки route/UI нужен локальный test environment с зависимостями. Шаги: установить зависимости в отдельном локальном окружении; выполнить `node --experimental-test-module-mocks --import tsx --test src/app/api/integrations/oauth/result/__tests__/routeGet.test.ts`; затем смонтировать `IntegrationsModal` с известным Google-подключением и подать ответы 200 без `integrations` и ошибку запроса — проверить, что `hydrateConfirmedOAuthIntegrations` не вызван, подключение осталось, а toast показан. Это не требуется для доказанного локального parser boundary и не выполнялось здесь.

## Что заявлял автор и что проверено

Заявлено автором: фикс только в `src/lib/auth/oauthIntegrationPersistence.ts`, плюс malformed-body тест и перенесённый отрицательный тест 3873. Это подтверждается diff: изменён parser, добавлены `src/lib/auth/__tests__/oauthIntegrationMalformedBody.test.ts` и `scripts/__tests__/web573-acceptance-negative.test.mjs`.

Доказано мной:

- refs существуют: авторская `c4aa8e4cefaa26fdc91cd611fc740dad092fb4a2`, предыдущая приёмка `0679542f950e1227f453dd1e92432e8bed0001f7`;
- worktree приёмки создан на авторском коммите; итоговая ветка содержит также мой acceptance-тест в `30007dd6a`;
- `node --import ./scripts/ts-node-resolver.mjs --test src/lib/auth/__tests__/oauthIntegrationMalformedBody.test.ts` — `RC=0`, прошли все 5 тестов;
- `node --import ./scripts/ts-node-resolver.mjs --test scripts/__tests__/web573-acceptance-negative.test.mjs` — `RC=0`, прошёл 1 тест;
- `node --import ./scripts/ts-node-resolver.mjs --test src/lib/auth/__tests__/oauthIntegrationStatusRead.test.ts` — `RC=0`, прошли все 6 тестов;
- мой `scripts/__tests__/web573-malformed-body-independent.test.mjs` — `RC=0`, прошли все 3 теста;
- тот же мой тест на старом `d85bb2dad15a3ba52c773b0a2362748009a2c3b9` — `RC=1`: отсутствующее поле снова стало `{ ok: true, integrations: [] }`. Это требуемый красный контроль старого кода.

## Независимый обход всех мест

Искал не только по имени файла, а по цепочке: HTTP GET ответа → `json()` → чтение `integrations` → передача в состояние.

Нашёл один production parser этой проекции:

- `src/lib/auth/oauthIntegrationPersistence.ts:113` делает GET;
- `:126` читает JSON;
- `:136-145` отбрасывает не-2xx, пустое тело и отсутствие `generation` как `request-failed`;
- `:153-159` требует именно массив `integrations`; только настоящий массив, в том числе пустой, даёт `ok: true`.

Единственный потребитель — `src/components/modals/IntegrationsModal.tsx:186-202`: hydrate находится только в `if (result.ok)`, а прочие ошибки ведут к toast. GET route (`src/app/api/integrations/oauth/result/route.ts:63-66`) возвращает `Object.values(state.integrations)` и не имеет fallback `|| []`.

`src/store/useStore.ts:1265-1309` — отдельный localStorage mirror, а не разбор внешнего HTTP-ответа; неверная форма там даёт `null`, не пустой authoritative projection. Другие вхождения `integrations` относятся к состоянию, UI, route tests или POST payload и не являются ещё одним GET parser-ом этой проекции. В evidence сохранён полный grep-обход.

Вывод: пропущенного production места с опасной семантикой «нет поля → пустой список → стираем известное подключение» не найдено.

## Что именно проверяет каждый тест

- `oauthIntegrationMalformedBody.test.ts`: missing field, честный пустой массив, неправильный тип поля, фильтрация мусора внутри массива и HTTP 500. Это не тавтология: каждый assert сравнивает результат loader-а с заранее заданной ожидаемой формой.
- `web573-acceptance-negative.test.mjs`: дословный отрицательный контроль 3873 — 200 без `integrations` обязан быть `request-failed`.
- `oauthIntegrationStatusRead.test.ts`: успешное подключение, `not-bound`, 500, network reject, 200 без `generation`, не-JSON. Он проверяет старую часть WEB-573 и отсутствие регресса.
- `web573-malformed-body-independent.test.mjs`: независимые 200 без поля, 503 и 200 с `integrations: []`; отдельно проверяет, что ошибочный результат сам не содержит поля `integrations`, которое можно было бы принять за пустую замену. На старом коде missing-field case красный.

Тавтологии вида `assert.equal(X, X)` в проверенных тестах не использованы. В моём тесте после аудита удалён слабый вариант сравнения, который сводился бы к `known` против `known`.

## KNOWN ISSUES

1. `routeGet.test.ts`, `oauthAtomicWiring.test.ts` и `web573DriveMenu.test.ts` не запустились: Node сообщил `ERR_MODULE_NOT_FOUND: Cannot find package 'tsx'`. Нужна локальная установка зависимостей и повтор этих команд; production/БД для этого не нужны.
2. Браузерный render не доказан. Проверено ветвление исходника; для render-сценария нужны локальные UI test dependencies и mock fetch.
3. Реальный сервер сегодня не проверялся и не должен был проверяться по условиям волны. Его текущий GET route по коду всегда формирует `integrations` из `Object.values`; фикс защищает от malformed body, proxy или рассинхрона версий.

## Evidence и артефакты

- Evidence: `/Users/limamarty/waves/3886ACC573-evidence/`.
- SHA256 evidence: `SHA256SUMS` в этой папке.
- Bundle: `/Users/limamarty/waves/3886ACC573.bundle`.
- Bundle hash: `93b8a1227fa9a71f0fcfa6fa284ef2d9d59e11d9a1e90eb66d117d292e03e1f9` (файл `/Users/limamarty/waves/3886ACC573.bundle.sha256`).
- Bundle сделан как `HEAD --not refs/waves/l115n`; `verify` и `list-heads` выполнены успешно. Head bundle: `30007dd6ae6b9e07a814da2c00bf398b8017128d`.
2026-09-15T15:47:19.259Z · coordinator
[15.09 15:47Z координатор] M1/DeepSeek 4064 read-only post-QA reconciliation: INCOMPLETE, пропущенного DONE нет; SHA evidence 5/5. На точном source l115o присутствуют terminal outcome discrimination, Article-28 role split и явный toast, но malformed-body fix 3880 отсутствует как объект, а живого Google OAuth прогона на l115o нет. Узкий остаток: один authenticated production connect, ровно один `/api/integrations/google/auth` fetch, terminal explicit outcome и connected state. WEB-573 остаётся review до live receipt.
2026-09-22T18:07:02.407Z · triage-neo
РЕШЕНИЕ=in_progress
ОСНОВАНИЕ=Приёмка 4064: INCOMPLETE; на exact l115o есть terminal outcome discrimination и Article-28 role split, но live Google OAuth receipt не записан.
ЧТО НУЖНО=Один authenticated production connect: ровно один fetch /api/integrations/google/auth, terminal explicit outcome и подтверждённое connected state на обслуживаемой линии.
triage-neo 4604
2026-09-23T12:11:33.366Z · triage-neo
РЕШЕНИЕ=parked
ОСНОВАНИЕ=2026-09-15, волна 4064 INCOMPLETE; текущая линия l115o, но malformed-body fix c4aa8e4ce не установлен и live Google OAuth receipt отсутствует.
ЧТО НУЖНО=первый шаг: один authenticated production connect с одним fetch /api/integrations/google/auth и подтверждённым connected state; ПРЕДЛОЖЕНИЕ=split
triage-neo 4704
2026-09-27T15:30:33.187Z · coordinator
[27.09 15:30Z координатор] Взят в работу по слову владельца 27.09 15:28Z. Фикс malformed-body c4aa8e4ce перенести на текущую линию r16, тест, затем в сборку; живое подключение Drive — после посадки.
2026-09-27T15:44:07.010Z · coordinator
[27.09 15:44Z координатор] [27.09 15:50Z координатор] Полный след поиска фикса 3880, для нулевого агента.
ЧТО ИСКАЛ: коммит c4aa8e4ce (волна 3880, 14.09, ветка wave/3880-web573-malformed-body на базе l115n d85bb2dad), принят независимой приёмкой 3886 (GO).
ГДЕ ИСКАЛ И НЕ НАШЁЛ: A2 /home/ubuntu/nc-build (git cat-file — нет объекта, refs с 3880 нет); A1 все репо /home/ubuntu/nc*, /srv/nc, /home/wave/* — нет; M1 все wt-l115s-* — нет; M1 ~/waves/3880* — нет каталога. Единственный след — M1 ~/Downloads/merged-en/3880.txt (текст отчёта) и комменты этого тикета 14.09 15:51Z/16:05Z. Сверка 4064 (15.09) уже фиксировала: на l115o объекта нет. Вывод: волна 3880 не была посажена, дерево волны удалено при чистке.
СУТЬ ФИКСА (из коммента 3880/3886): ответ сервера о подключениях с полем integrations=[] → честное «нет подключений» (ok:true); поле отсутствует / не массив / элементы неверного вида → явная ошибка проверки состояния, а НЕ «Не подключено».
ЧТО ДЕЛАЕТСЯ: волна 5116 (Neo, Luna high) переносит заново на текущую линию r16 @ 79bbf331 (integrate/5110-r16), сначала проверяет — вдруг уже есть в другом виде. Тесты RED→GREEN, adversarial. Сдача ~/waves/5116WEB573MALFORMEDBODY-REPORT.md.
УРОК: принятый GO без посадки теряется при чистке деревьев. Принятые фиксы сразу класть в ветку на A2 nc-build (как сейчас делаю: l115s-web*).
ОСТАЁТСЯ после посадки: живой прогон с реальным Google OAuth — один вход, ровно один fetch /api/integrations/google/auth, явный исход и состояние connected.
2026-09-27T15:46:01.647Z · coordinator
[27.09 15:46Z координатор] [27.09 16:45Z координатор] Волна 5116 (Neo, Codex Luna high) VERDICT=GO. Проверено: SHA256SUMS 22/22 OK, bundle verify OK.
КОД: A2 /home/ubuntu/nc-build ветка l115s-web573-malformed-body = 46edc90af2 поверх r16 79bbf331 (1 коммит). Патч: neo ~/waves/5116WEB573MALFORMEDBODY-evidence/0001-*.patch.
ЧТО ИЗМЕНЕНО: src/lib/auth/oauthIntegrationPersistence.ts — успешный ответ о подключениях считается корректным ТОЛЬКО если integrations — массив. [] = честное «нет подключений» (ok:true). Нет поля / null / объект / строка / неверный элемент → существующий исход request-failed (тот же механизм, что остальные ошибки подключения), а НЕ «Не подключено». Новые тексты не вводились.
ТЕСТЫ: новый src/lib/auth/__tests__/oauthIntegrationMalformedBody.test.ts. До: loader/UI/wiring 16/16, OAuth result GET 3/3, POST 7/7, Google exchange 3/3, auth route 1/1. После: всего 39/39 (loader+malformed 25/25). Adversarial: вернули старый fallback → 7 из 9 случаев RED (старое поведение давало ok:true с [] или молча отфильтровывало мусор). scoped tsc (tsconfig.web542atomic-scoped.json) новых ошибок 0.
TROUBLESHOOTING: scoped tsc на дефолтной куче Node падает OOM — запускать с NODE_OPTIONS=--max-old-space-size=8192 (это не ошибка типов). Для legacy route тестов нужен локальный prisma generate (без БД).
ОСТАЛОСЬ: независимое ревью → войти в r17 (вместе с WEB-689 l115s-web689-resend-events acc53616). После посадки — живой прогон с настоящим Google OAuth (один вход, один fetch /api/integrations/google/auth, явный исход, connected).
2026-09-27T16:07:37.316Z · coordinator
[27.09 16:07Z координатор] [27.09 17:10Z координатор] Независимое ревью 5117 (M1, gpt-5.6-sol high) VERDICT=GO, evidence SHA256SUMS OK. Найдено и исправлено (тест RED→GREEN):
- WEB-689 HIGH: жёсткий bounce Resend приходит как bounce.type="Permanent", а код подавлял адрес только для "hard" → адрес не блокировался. Исправлено.
- WEB-689 MEDIUM: секрет/подпись svix в base64 принимались с мусором в конце. Теперь только канонический base64.
- WEB-573 HIGH: ответ {type:"google"} без остальных полей считался валидным и превращался в «Не подключено». Исправлено: минимально валидная запись сохраняется, неполная = ошибка.
PASS: HMAC по сырому телу, timingSafeEqual, несколько v1, окно ±300 с, fail-closed без секрета, 401 на неверной подписи, идемпотентность по svix-id (гонка), complaint/Permanent блокируют sendEmail до транспорта, неизвестный тип 2xx; логи без адресов/секретов; новой миграции нет. Один парсер ответа подключений и один UI-потребитель.
Итог тестов: 83/83 focused, scoped tsc RC0, adversarial RED подтверждён.
ОСТАТОК (LOW): у нового подписанного маршрута общий долг rate-limit/monitoring API-периметра.
КОД: A2 nc-build ветка integrate/5118-r17 = 1f78507e (r16 79bbf331 + 7 коммитов). Патчи ревью: m1 ~/waves/5117REVIEWR17-evidence/000[1-5]*.patch.
ДАЛЬШЕ: сборка r17 = волна 5118 (A2, стартует после деплоя r16 5115) → деплой на стенд → посадка. После посадки: создать вебхук в панели Resend на /api/email/webhook/resend (секрет whsec_ в env прода) и живой вход Google.
Воркер
не проверен landed:l115e-feature-QA-pending a1nc движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
Обновлён
2026-09-27T15:46:01.968Z