golden-migration

Міграція Golden

Новий project-golden, project-application-api, Gateway та Electron вводяться поруч із чинними stack-golden і Golden Electron. Production перемикається частинами, без big bang.

Незмінні правила

  1. Legacy collections, queues та endpoints не змінюємо до переведення відповідної логіки.
  2. Нові consumers використовують окремі versioned queues та commands.
  3. Read-only sync можна виконувати паралельно для порівняння з legacy.
  4. Partner-write для конкретної TU та операції виконує лише legacy або нова система, але не обидві.
  5. Workers мають idempotency, lock і явний owner.
  6. Rollback перемикає routing назад; canonical дані не видаляються.
  7. Джерело істини для зв’язку TU↔користувач — колекція TU. details.ladies у користувача пишемо для сумісності, але не читаємо і не віддаємо на фронт з нових ендпоінтів. Деталі — «Зв’язок TU ↔ користувач».

Одиниця міграції

Мігруємо не весь stack-golden одним релізом і не окремий endpoint без його залежностей. Одиниця міграції — завершена capability: frontend route, canonical data, partner operation, background writer, logs і post-effects.

Для capability, яку вже переключили:

  1. Frontend звертається лише до project-application-api.
  2. Canonical collection має одного owner-а запису для конкретних полів та операцій.
  3. Старий публічний route вимкнений або тимчасово проксіює команду у V2; він не виконує власний write.
  4. Ще не перенесений legacy-код читає canonical collection через один compatibility repository.
  5. Старі довільні Mongoose models не направляємо на нову collection: назви полів, шифрування та семантика query несумісні.
  6. Після parity, pilot і rollback period legacy collection та bridge цієї capability видаляються.

Постійного двостороннього sync між legacy і canonical collections не робимо. Тимчасова проєкція допустима лише як короткий rollback-bridge з метрикою lag і явною датою видалення.

Співіснування V2 і legacy

flowchart LR
    Front["Frontend"] -->|"перенесені routes"| V2["project-application-api"]
    Front -->|"решта routes"| Stack["stack / stack-golden"]
    V2 --> Project["project-golden"]
    Project --> Partner["Golden partner API"]
    V2 --> Canonical[("project_admins / project_tus")]
    Project --> Canonical
    Stack --> Compat["LegacyGolden compatibility repositories"]
    Compat --> Canonical
    V2 -->|"лише неперенесені post-effects"| LegacyEffects["versioned legacy commands"]
    LegacyEffects --> Stack

Compatibility repository є anti-corruption layer: він робить конкретні canonical query, розшифровує secret на межі та повертає стару форму тільки legacy consumer-у. Його не імпортуємо хаотично по файлах — старі сервіси отримують вузькі методи на кшталт findActiveAdmins(), findTUCredentials() і findTUsByPartnerIds().

Допускається кілька writer-ів однієї collection лише для різних, явно зафіксованих полів. Наприклад, поки TU Card живе у Stack, Stack володіє зв’язком KM, а V2 refresh — partner profile/status. Два сервіси не можуть одночасно змінювати одне assignment або partner password.

Середовища

КонтурХто працюєДані й доступ
Legacy productionЧинні Stack, Golden service та ElectronПоточні endpoints, queues і collections
V2 testНові project-application-api, Gateway, Golden service та ElectronTest endpoints, окремі queues/collections, test operators і TU
V2 shadowНові server services без live-writeЧитають production-compatible дані й порівнюють результат із legacy
V2 pilotAllowlist операторів/TULive-write дозволений лише для явно переведеного scope
V2 productionНові servicesLegacy лишається rollback-контуром до завершення міграції

Test Electron має окремі appId, product name, user data directory, API origin, socket namespace та update channel. Його можна встановити поруч із production Electron.

Етапи

№ЕтапРезультатУмова переходу далі
01Packages і tooling@it-monkeys/contracts, @it-monkeys/backend-common та project-platformPackages збираються через link, pack і ручний release
02Каркасиproject-application-api, Gateway і project-golden розгорнуті без production routingHealthcheck, auth, RMQ і observability працюють
03Admins і TU read modelНовий Golden читає partner API та пише canonical Admin/TUПовторний sync і backfill не створюють дублікати
04Shadow comparisonLegacy і V2 результати порівнюються автоматичноУзгоджені counts, status, assignments і profile fields
05Test ElectronНова програма працює на V2 test із тестовими TULogin, startup, reconnect, sync та workspace проходять сценарії
06Production pilotКілька операторів/TU переведені через allowlistНемає подвійних writes, втрати actions/tasks і деградації partner API
07Вертикальна міграціяФічі переходять по одній із власним flag і rollbackКожна фіча пройшла parity та pilot
08РозширенняAllowlist поступово збільшуєтьсяМетрики й support стабільні на кожному кроці
09Вимкнення legacyСтарі workers/routes зупинені, repository архівується пізнішеПовний scope працює на V2 і минув rollback period

Фактичний стан на 2026-08-27

ЧастинаУже єЩо блокує production cutover
Packagescontracts, backend-common, canonical schemas/repositoriesRelease packages і точні production versions
Deployablesproject-application-api, project-golden, healthcheck, Stack JWT, internal HTTPProduction config, secrets, network policy та telemetry
Adminsmigration, list/resolve/create/update, Golden credentials validationLegacy admin compatibility repository, TU count, routing flag
TU catalogmigration, Golden list adapter, compare/refresh, canonical updateRead endpoints, shadow/dry-run, legacy TU compatibility reads
TU passwordGolden partner mutation і encrypted canonical passwordLegacy Electron/workspace повинні читати canonical credentials
Assignmentsтімлід/operator batch flow, stale-state check, user/TU updateTasks, Electron effects, canonical logs і production parity
KMservice method синхронізації зі Stack TU CardВизначити Stack command; refresh не має знімати KM
Logsспільна tu_logs через TuLogRepository: рольові події призначень і lifecycle refresh—

Поточні migration endpoints є apply-командами, а не безпечним shadow: повторний запуск переносить значення зі старих collections і може перезаписати нові canonical зміни. Перед production вони мають отримати dry-run/compare, deployment guard і бути вимкнені одразу після cutover.

Перший production vertical slice

frontend Admin/TU routes
  → project-application-api
  → internal HTTP
  → project-golden
  → Golden partner API
 
project-application-api ↔ canonical project_admins / project_tus
stack-golden → compatibility repositories → ті самі canonical collections

На цьому тижні реалістичний production scope:

ЧергаCapabilityРезультатЧи можна закрити повністю
0Dark deploy двох сервісівHealth, auth, internal network, secrets та індекси перевірені без frontend traffic✅
1AdminsFrontend CRUD працює через V2; project_admins — єдине джерело; legacy лише читає через adapter✅
2TU catalog/readBackfill, read API та shadow comparison працюють на project_tus; legacy Electron читає canonical TU✅ після compatibility reads
3TU passwordV2 є єдиним writer partner/canonical password; legacy workspace читає розшифровані canonical credentials✅ після credentials adapter
4TU refresh writeСтарий LadySyncService вимкнений; V2 володіє profile/status/admin✅ лише після detach post-effects
5Manual assignmentsДо cutover legacy flow пише assignments у canonical TU через compatibility repository; після cutover усі TU/user/log/task/Electron effects переходять у V2❌ V2 не вмикати до parity
6Electron workspace, statistics, favorites, senderЗалишаються у stack-golden і старому Electronокремі наступні vertical slices

Отже, самі сервіси можна безпечно залити цього тижня. Frontend routing треба вмикати окремими flags: admins, tu-read, tu-password, tu-refresh, tu-assignments. Один загальний goldenV2=true для всього сервісу не використовуємо.

Міграція Admins

project_admins стає єдиним source of truth. Старий golden_admins не можна просто замінити імпортом schema: legacy очікує id_api, відкритий password, isBlocked і підтримує довільні query, а canonical document має login, passwordEncrypted і status.

У stack-golden є 13 файлів із прямим import AdminModel, плюс залежності від legacy IAdmin. Вони покривають CRUD, sync/statistics, Electron/API operations і тести. Для завершеного cutover:

  1. Додати один LegacyGoldenAdminRepository поверх ProjectAdminRepository.
  2. Реалізувати лише фактично потрібні typed methods; не переносити legacy endpoint із довільним Mongo query.
  3. Замінити прямі imports AdminModel у runtime-коді на repository.
  4. У коротке maintenance-вікно зупинити legacy Admin writes, виконати backfill зі збереженням _id і звірити count/login/status/hash credentials.
  5. Переключити frontend Admin routes на V2; старий Admin CRUD вимкнути або зробити proxy без власного write.
  6. Вимкнути Admin migration endpoint і залишити golden_admins тільки як rollback snapshot на обмежений строк.

Compatibility spike вже підтвердив роботу @it-monkeys/contracts і @it-monkeys/backend-common з legacy Mongoose 6.2.2. Не можна неявно оновлювати Mongoose у stack-golden: підняття lock-версії вже спричиняло сторонні type errors.

Міграція TU

TU складніша за Admin: GoldenLadyModel читають і змінюють Electron, favorites, metrics, statistics, workspace, sync та assignment flows. Тому підміна model на project_tus небезпечна.

Робимо один LegacyGoldenTURepository з вузькими projections:

  • знайти TU/credentials за partner ID;
  • знайти canonical admin і partner admin ID;
  • отримати TU за списком partner ID або Stack Card ID;
  • оновити лише ті legacy-owned поля, власник яких зафіксований у migration matrix.

Порядок cutover:

  1. Додати POST /v2/tus/find і, за потреби frontend, POST /v2/tus/findById з role scope.
  2. Додати dry-run TU migration та shadow compare без canonical write.
  3. Перевести legacy runtime reads, необхідні Electron/workspace, на LegacyGoldenTURepository.
  4. Зупинити старий TU sync, виконати фінальний backfill зі збереженням _id, звірити profile/status/admin/assignments/password presence.
  5. Увімкнути V2 read і password окремими flags.
  6. Увімкнути V2 refresh write лише після того, як видалення/деактивація виконує всі operator detach effects.
  7. Manual assignment routes залишити legacy до готовності tasks, Electron SET_LADY/DROP_LADY та canonical logs; потім перемкнути всією capability за один cutover.

Поки manual assignments належать legacy, вони вже повинні змінювати project_tus.assignments через compatibility repository, а не продовжувати запис у golden_ladies. Інакше V2 read одразу стане застарілим.

KM не входить у системний detach: його canonical зв’язок оновлюється тільки під час зміни TU Card у Stack.

Борг після вимкнення legacy TU sync

2026-08-28 зі stack-golden видалені LadySyncService, CheckGoldenLadiesWorker і UpdateDataGoldenLadiesWorker: їхню роботу виконує кластерний TU refresh у project-application-api. Разом із ними зникли два post-effects, які новий refresh поки не повторює.

№Що зниклоДе булоКоли повернути
1Автоматичний initFavorites() після синхронізації TULadySyncService.processLadyUpdatesПід час міграції favorites і вигрузки статистики: крок має отримати FavoriteService
2Запис lifecycle-подій TU (created, deleted, activated, deactivated)LadySyncServiceЗакрито: refresh пише їх у спільну tu_logs, яку читають і legacy statistics

Ручний роут GET /initFavorites у stack-golden лишився, тож до міграції favorites ініціалізацію можна запускати вручну.

Favorites: стан

2026-09-03 з’явились canonical collections ProjectFavorite і ProjectGlobalFavorite: schema, indexes і repositories у backend-common, типи у contracts, FavoritesModule і POST /migrations/favorites/apply у project-application-api. Writer поки лишається legacy: golden_favorites, golden_global_mans і golden_man_profiles живуть без dual-write, а canonical наповнюється тільки міграцією.

Порядок далі: імпорт статистики → ProjectFavoriteService як єдиний writer платної активності → FavoriteService і GlobalFavoriteService з API → переключення Electron snapshot → вимкнення legacy favorites. Нотатки favorites (note) і language у canonical не входять і чекають NotesService та окремого рішення.

Логи TU для всіх проєктів живуть в одній колекції tu_logs (TuLogRepository у backend-common): платформа пише туди рольові події призначень і lifecycle-події refresh, legacy-сервіси читають їх через свої адаптери. Старі log_ladies, log_lady_events і golden_lady_event_logs більше не пишуться.

Борг: online оператора у /v2/team/find

POST /v2/team/find віддає структуру команди без isOnline і sleepMode. Frontend поки бере online зі старого джерела, тому сторінка тимчасово живе на двох запитах.

Причина: online зберігається в масиві в пам’яті процесу stack-golden (src/socket/users/onlineUsers.ts), який наповнюють socket-з’єднання. Ні Redis, ні Mongo його не містять, тому project-application-api як окремий stateless-кластер прочитати його не може.

Тимчасовий RMQ-запит у stack-golden свідомо не робимо: це завело б у новий сервіс залежність, яку одразу довелось би знімати.

Повернути разом із OperatorOnlineService: коли Electron Gateway почне писати OperatorOnlineSession, /v2/team дочитує сесії й додає два поля у ProjectTeamGroupDto.operators[]. Контракт при цьому не ламається — поля з’являються, а не змінюють форму.

Борг: post-effects Electron у v2

Закрито: усі post-effects v2 ідуть через Electron Gateway, роль якого поки виконує stack-golden (internal/electron-gateway). Платформа шле лише факти, а команди програмі оператора будує Gateway:

Операція v2Route GatewayЩо робить Gateway
Зняття TU з користувача: команда зі Stack, refresh, зміна призначеньdetached-tusDROP_LADY, logout mailer-сокета, deleteBadInvites знятого тімліда
Призначення TU оператору у змініassigned-tusSET_LADY з профілем і паролем TU; зайняту анкету ставить у чергу на зміну
POST /v2/tus/updateProjectData з findNewtu-project-dataUPDATE_SENDER_TYPE операторам усіх змін TU
Блокування або переведення оператораkickперезапуск програми; шле сам Stack

Недоступність Gateway не валить операцію: зміни вже записані, помилка лише логується. З появою окремого Electron Gateway зміниться лише адреса ELECTRON_GATEWAY_URL, за межами платформи.

Live-стан теж читається через Gateway: online-operators віддає операторів із підключеною програмою (статистика, isOnline). Без Gateway — нікого, запит статистики не падає. Legacy stack-* загортає відповідь у { success, data } без code; InternalHttpClient платформи приймає обидва формати.

Борг: reset пароля TU для Electron

POST /golden/mail-extension/resetLadyPassword міняє пароль однієї TU і повертає новий пароль оператору: Electron ним логіниться на партнерський сайт. PATCH /v2/tus/password міняє паролі пачкою і навмисно нічого не повертає, тому прямою заміною не є.

Переносимо разом з Electron API; до того часу роут залишається у stack-golden.

Інвентар залежностей від golden_ladies

Розклад перед підміною GoldenLadyRepository на canonical. stack-golden звертається до колекції у 20 файлах: ~50 читань через репозиторій і 19 повз нього.

МодульЗверненьРішенняПримітка
GoldenLadyRepository3місце підмінивже anti-corruption layer: фільтр-DSL і join level/historyLevel зі Stack
LadyService — логіка роутів~12видаляєтьсяразом із роутами LadyController
LadyService — set/drop тімліда й оператора4 записиадаптерне помирають з роутами: їх кличуть системні detach-флоу
LadyService.autoResetLadyPassword1 записадаптермігрує разом з mail-extension
workspace8вузький метод credentialsусі вісім — це пароль TU для partner API
ElectronApi.checkLadiesOnline1лишаєтьсястарий Electron досі викликає
ElectronApi — решта4адаптердо переходу на новий Electron
MailExtension.setNAFStopTime1 записперевірити актуальність полястарий відголосок розширення
MailExtension — решта, extension4адаптер
favorites10адаптермігрує разом зі статистикою
StatisticsV2, statistics, new-year12адаптер
Metric7адаптер
OnlineTime, OnlineAnalysis5адаптер
Dashboard, ErrorReport, ai-notes, operator, supervisor, socket/electron8адаптер

Живого тут майже все, тому адаптер однаково має вміти повний legacy-документ. Чистка зменшує кількість викликів, але не обсяг адаптера.

Стан підміни

GoldenLadyRepository читає project_tus і віддає стару форму IGoldenLady. Колекції golden_ladies більше не торкається жоден сервіс — модель лишилась тільки як джерело типу.

Що зійшлося при звірці 1003 legacy TU з canonical:

  • _id збігаються один-в-один: міграція зберегла ідентифікатори, тому зв’язки за ladyId у golden_lady_event_logs та інших колекціях працюють без змін;
  • паролі: 1003 з 1003 розшифровуються з credentials.passwordEncrypted і збігаються з legacy plaintext;
  • name, stackId, clientManager, findNew, stopNafTime — нуль розбіжностей;
  • supervisors/operators: у legacy завжди 3 позиції, у canonical завжди зміни 1,2,3 — мапінг позиційний за номером зміни;
  • age і photo розходяться в 53 і 66 TU: canonical свіжіший, бо legacy-профіль оновлював уже мертвий updateDataLadies;
  • isDeleted розходиться у 249 TU: canonical правий, у legacy баг.

lastUpdateActive і lastUpdateDeleted віддаються порожніми — у canonical їх немає і ніхто їх не читає. level та historyLevel як і раніше приходять зі Stack по RMQ.

Системні detach-флоу

OperatorService.dropAllLadiesFromOperators і SupervisorService.dropAllLadiesFromSupervisor знімають TU при блокуванні або видаленні користувача. Вони викликають ті самі dropGoldenLadyFrom*, що й роути призначень, тому не зникають разом із LadyController і потребують окремої відповідності у v2.

Зв’язок TU ↔ користувач

Зв’язок «яка TU за ким закріплена» зберігається двічі: на боці TU (assignments.shifts[] у canonical, operators/supervisors у legacy) і на боці користувача (users.assignments[].details.ladies). Обидві сторони пишуться окремими запитами, між ними немає транзакції, а в Golden ще й через RMQ.

Правило. Джерело істини — сторона TU. Нові й оновлені логіки читають призначення з колекції TU; details.ladies лишається тільки як запис для сумісності зі старим кодом і на фронт з нових ендпоінтів не потрапляє. Запис прибираємо останнім кроком, коли жодне читання його не використовує.

Чому сторона TU

  • Призначення — атрибут TU: номер зміни (1–3) є властивістю анкети, у користувача такого поняття немає. Тому зі сторони користувача неможливо відповісти «на якій зміні», а зі сторони TU — можна. Все, що вміє сторона користувача, сторона TU вміє теж; зворотне хибно.
  • Кардинальність асиметрична: у TU рівно три слоти, у оператора в середньому 12 TU і до 67.
  • Запис стає одним оновленням одного документа замість двох незалежних.
  • TU_SCOPE у project-application-api уже побудований на assignments.shifts.*.

Стан у базі

Одна логіка реалізована чотирма несумісними способами:

ПроєктСторона TUСторона користувача
goldenoperators[3], supervisors[3] — строго 3/3 у всіх 1003details.ladies: number[] — id_api, не mongo id
udateoperators[3], supervisors[3] + currentOperatordetails.ladies: string[]
primesupervisor[] довжиною 2 у 332 і 3 у 201; поля для операторів немаєdetails.ladies: string[]
chathouseдубль полів operator/operators і supervisor/supervisors; пишеться однина, operator порожній у 675 з 676details.ladies: string[]

stack-commons описує IChathouseOperatorFamily.ladies і chathouse_ladies.operators, яких у базі фактично немає — типи тут не відповідають даним.

Окремо стоїть stack_ladies.assignedForEmailUserIds (97 TU) — ще одне посилання TU→користувачі, у п’ятій формі.

Звірка сторін на production-дампі

Зв’язокПарРозходжень
golden operator8650
golden тімлід8980
prime тімлід2420
chathouse тімлід12470
client manager, усі 4 проєкти15450
udate operator807 / 83023 висячі на боці TU
udate тімлід804 / 8073 висячі

Масової розсинхронізації немає. Усі 26 розходжень — у udate: 12 припадають на assignment із isDeleted: true (працівника зняли, з користувача прибрали, з TU ні), решта — живі assignment без TU у масиві. Три випадки з тімлідом — на одному користувачі.

Важливіше за розходження те, що 1489 зв’язків (prime 242, chathouse 1247) існують лише на боці користувача: у цих проєктах TU не знає свого оператора взагалі. Тому для них це не дублювання, а єдине джерело, і зняття потребує спершу міграції даних у shifts.

Інвентар читань

stack-golden — переведено на сторону TU. Вісім точок, усі ізольовані, бо сервіс обслуговує один проєкт:

МісцеБуло
supervisor/SupervisorService.getLadyApiIdsOnFamilySupervisorRMQ FamilySupervisorFindOne → .ladies; три споживачі: GoldenFavoritesService, global-man.service ×2
supervisor/SupervisorService.dropAllLadiesFromSupervisorgetLadies({ api_ids: supervisorFamily.ladies })
operator/OperatorService.dropAllLadiesFromOperatorsRMQ по оператору → getLadies({ api_ids })
favorites/GoldenFavoritesService.getEmailFavoritesByOperatorForSupervisorRMQ → .ladies → $in
extension/GoldenExtensionService.getLadyMessageStatusгілки тімліда/оператора → .ladies
extension/GoldenExtensionService.getLadyMailStatusте саме, копія
StatisticsV2/StatisticsServiceV2 ×3familyOperator.ladies → importantLadyIds
debug/debug.service.ladies.length для класифікації WS/Electron/Extension

OnlineTime/OnlineTimeService до цього списку не належить: operator.ladies там — поле власного документа OnlineTimeModel, а не користувача.

stack — не чіпаємо. Близько 60 звернень у 14 файлах, і жодне з них не є golden-only: скрізь або switch (family) зі спільним входом, або clientManager[family].ladies, або familyOperator.ladies, де проєкт — параметр. Часткова заміна для одного проєкту означала б розгалуження в двадцяти місцях і два джерела істини в одній функції.

КатегоріяДе
записи (лишаються з TODO)UserRepository 1482/1507/3483/3507, FamilyOperatorController, FamilySupervisorController, StackClientManagerService
зворотній пошук «хто працює з цією TU»UserRepository 1287, 1818, 2384, 2968, 3460
тікетиUserRepository 4647–5037, ticket-tu.service, ticket-assignee.service, ticket.service
лічильники для UIStackHrService, StackSupervisorService2, StackUserService, StackTopManagerService
списки TU користувача для UIStackOperatorService, stack/components/ladies/LadyService
client managerStackClientManagerService

Точка для наступних проєктів. Усі перелічені читання отримують .ladies не з бази напряму, а з шести місць у UserRepository: _reconstructLegacySupervisor, _reconstructLegacyOperator, _reconstructFamilySupervisorFromAssignment, _reconstructFamilyOperatorFromAssignment, _reconstructLegacyClientManager і агрегація findFamilyUsers. У двох із них поле протікає неявно, через ...details. Коли сторона TU з’явиться в усіх проєктах, правити треба буде саме ці шість місць, а не шість десятків споживачів.

Індекси

Читання за призначенням спирається на project_tu_operator_assignments, project_tu_supervisor_assignments і project_tu_client_manager_assignments. Вони описані в projectTUSchema, але project-application-api працює з autoIndex: false, тому на середовищі їх може не бути — у production-дампі станом на 01.09.2026 project_tus мала лише _id. Синхронізація виконується окремою командою:

npm run db:indexes:sync

Її треба прогнати на кожному середовищі перед тим, як читання за assignments.shifts.* піде під навантаженням. stackId індексу поки немає — додати, коли зʼявиться гарячий пошук за ним.

Латентна пастка

findSupervisorsByLadyFamilyIds, findClientManagersByLadyFamilyIds і findOperatorFamiliesByLadyId фільтрують assignments.role та assignments.details.ladies без $elemMatch — умови можуть збігтися на різних елементах масиву. Зараз не стріляє (жоден користувач не має перетину TU між assignment різних ролей), спрацює, щойно хтось стане одночасно CM і оператором в одному проєкті. Індексу на assignments.details.ladies немає, тому ці запити ще й скануються повністю по 16 332 користувачах.

Shadow comparison

Поточний POST /v2/tus/refresh є writer і не використовується як shadow. Потрібна окрема read-only команда, яка отримує partner snapshot, викликає TUSyncService.compareSnapshot, повертає diff і нічого не змінює у Mongo, users, logs або partner API.

Для кожного shadow run зберігаємо або рахуємо:

  • кількість записів legacy/V2;
  • пропущені та зайві external IDs;
  • відмінності status і assignments;
  • hash нормалізованих важливих полів;
  • час і кількість partner requests;
  • помилки та повторні спроби.

Shadow не виконує password change, assignment, sender, task close, media upload, online ping або іншу partner mutation.

Production cutover checklist

  1. Зафіксувати package versions; не використовувати локальні links.
  2. Перевірити однаковий cipher key у project-application-api, project-golden і legacy compatibility layer.
  3. Запустити db:indexes:sync як окремий deployment job після перегляду diffIndexes; application instances не синхронізують indexes на startup.
  4. Закрити internal Project API від зовнішньої мережі.
  5. Обмежити або вимкнути apply-migration endpoints; роль director не повинна мати постійного доступу до повторного production backfill.
  6. Поставити feature flags у стан off, розгорнути обидва сервіси й перевірити health/partner test calls.
  7. Виконати dry-run, maintenance freeze, Admin backfill, потім TU backfill.
  8. Перевірити counts, IDs, status, admin relations, assignment relations і можливість розшифрувати вибіркові credentials без їх логування.
  9. Вмикати flags у порядку admins → tu-read → tu-password → tu-refresh; tu-assignments окремо.
  10. Для кожного flag мати owner, метрики, allowlist і одну дію rollback без видалення canonical data.

Перемикання Electron

Stack після login повертає, який workspace доступний оператору: legacy, v2-test або v2. TU, передана у V2 pilot, не повинна одночасно видаватися для роботи legacy Electron.

Gateway додатково тримає Redis lease на project + tuId. Lease захищає V2 від двох нових sessions, але головний захист від legacy/V2 конфлікту — routing і allowlist у Stack.

Перед розширенням pilot перевіряємо:

  1. Login, update і reconnect.
  2. Одна активна session на TU.
  3. Startup data та локальна БД.
  4. Chat/mail send із partner confirmation.
  5. Tasks, actions, online і sender sync з record-level ACK.
  6. Відновлення після падіння Electron, Gateway, API та проєктного сервісу.

Перемикання server-фічі

Кожна фіча має окремий flag із scope project, user або tuId:

legacy read/write
  → V2 shadow read
  → V2 read + legacy write
  → V2 read/write для pilot
  → V2 read/write для проєкту

Не всі фічі потребують проміжного V2 read + legacy write. Його використовуємо лише там, де V2 може коректно прочитати legacy результат.

Rollback

  1. Заборонити нові V2 sessions або requests для pilot scope.
  2. Дочекатися ACK активних sync batches і зупинити V2 writers/workers.
  3. Повернути Stack routing та frontend feature flag на legacy.
  4. Перепідключити оператора до legacy Electron.
  5. Зберегти V2 collections і telemetry для аналізу; не робити rollback через видалення даних.

Після rollback новий writer не запускається повторно, доки не перевірено незавершені tasks, sender operations і partner mutations.