[2026-05-14] work/plans/mattermost-migration.md — план миграции на корпоративный MM
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
---
|
||||
tags:
|
||||
- mattermost
|
||||
- migration
|
||||
- plan
|
||||
- personal-os
|
||||
created: '2026-05-14'
|
||||
status: in-progress
|
||||
---
|
||||
# Plan: Mattermost Migration
|
||||
|
||||
**Контекст**: Корпоративная политика требует перехода на corporate-hosted Mattermost.
|
||||
Zulip остаётся (апгрейд отменяется), но работа переезжает в MM.
|
||||
Personal/family/journal остаются в Discord и Zulip соответственно.
|
||||
|
||||
**Источник**: [Zulip тред «✔ Zulip migration ⭐»](https://zulip.qentra.top/#narrow/stream/4-master/topic/.E2.9C.94.20Zulip.20migration.20.E2.AD.90/near/15190)
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
| Пространство | Платформа |
|
||||
|---|---|
|
||||
| **Work** (Personal OS) | **Mattermost** |
|
||||
| Personal | Discord (без изменений) |
|
||||
| Family, journal | Zulip / Discord (без изменений) |
|
||||
|
||||
---
|
||||
|
||||
## Архитектура ботов
|
||||
|
||||
Проблема: сейчас executor-воркеры постят от имени Eagle → смешивает интерактив с автономной работой.
|
||||
Решение: отдельные bot-аккаунты с разными identity.
|
||||
|
||||
| Bot | Роль | Аватар |
|
||||
|---|---|---|
|
||||
| **Орёл** (Eagle) 🦅 | Персональный ассистент, интерактив, планирование | 🦅 |
|
||||
| **Executor** ⚡ | Автономные воркеры, code fixes, PR-отчёты | ⚡ или 🤖 |
|
||||
| **Brief** 📋 | Scheduled: daily brief, inbox triage, weekly review | 📋 |
|
||||
| *(опц.)* **Monitor** 🔔 | CI алерты, cron-ошибки | 🔔 |
|
||||
|
||||
Каждый бот = отдельный MM bot account = отдельный токен в `~/.hermes/.env`.
|
||||
|
||||
---
|
||||
|
||||
## Структура каналов (Categories → Channels)
|
||||
|
||||
```
|
||||
📁 Personal OS
|
||||
# general ← Eagle интерактив
|
||||
# daily-brief ← Brief bot
|
||||
# inbox ← Brief bot (inbox triage)
|
||||
# focus ← статус фокуса
|
||||
|
||||
📁 Executor
|
||||
# queue ← задачи в очереди, статусы
|
||||
# worker-1 ← лог активного воркера 1
|
||||
# worker-2 ← лог активного воркера 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Задачи
|
||||
|
||||
### 🔴 IT Ops (блокирующая)
|
||||
|
||||
**Задача для IT/Admin**: создать bot accounts в корпоративном MM.
|
||||
|
||||
Нужные боты:
|
||||
- `eagle-bot` — персональный ассистент (Eagle / Орёл)
|
||||
- `executor-bot` — автономные воркеры
|
||||
- `brief-bot` — scheduled доставка брифов
|
||||
|
||||
**Для каждого бота нужны**:
|
||||
- Bot Account (не incoming webhook — нужен постоянный токен и avatar)
|
||||
- Токен → передать Алексу для `~/.hermes/.env`
|
||||
- Права: читать/писать в каналах Personal OS и Executor
|
||||
|
||||
**Как проверить текущий доступ**:
|
||||
- В корпоративном MM: **☰ Main Menu → Integrations**
|
||||
- Если пункт есть → можно создавать самому
|
||||
- Если нет → запросить у MM Admin
|
||||
|
||||
---
|
||||
|
||||
### Технический план (после получения токенов)
|
||||
|
||||
**1. Выбор архитектуры**
|
||||
|
||||
| Подход | Когда использовать |
|
||||
|---|---|
|
||||
| Hermes gateway (MM адаптер) | Если нужен интерактивный Eagle (отвечает на сообщения в MM real-time) |
|
||||
| cron/scripts/prompt files | Только scheduled delivery (brief, inbox, executor-отчёты) |
|
||||
|
||||
Hermes уже имеет `gateway/platforms/mattermost.py` — WebSocket listener + REST API v4.
|
||||
Config: `MATTERMOST_URL`, `MATTERMOST_TOKEN`, `MATTERMOST_HOME_CHANNEL`.
|
||||
|
||||
**2. Конфигурация `.env`**
|
||||
|
||||
```bash
|
||||
# Mattermost
|
||||
MATTERMOST_URL=https://mattermost.company.com
|
||||
MATTERMOST_EAGLE_TOKEN=<eagle-bot token>
|
||||
MATTERMOST_EXECUTOR_TOKEN=<executor-bot token>
|
||||
MATTERMOST_BRIEF_TOKEN=<brief-bot token>
|
||||
MATTERMOST_TEAM_ID=<team id>
|
||||
# Channel IDs (узнать после настройки каналов)
|
||||
MM_CHANNEL_GENERAL=<id>
|
||||
MM_CHANNEL_DAILY_BRIEF=<id>
|
||||
MM_CHANNEL_INBOX=<id>
|
||||
MM_CHANNEL_EXECUTOR_QUEUE=<id>
|
||||
```
|
||||
|
||||
**3. Scheduled delivery (Eagle ← scripts)**
|
||||
|
||||
Для брифов и inbox triage — чистые скрипты, никакого daemon-а:
|
||||
```bash
|
||||
hermes --quiet --once "$(cat ~/agent/prompts/daily-brief.md)" \
|
||||
| curl -s -X POST "$MATTERMOST_URL/api/v4/posts" \
|
||||
-H "Authorization: Bearer $MATTERMOST_BRIEF_TOKEN" \
|
||||
-d "{\"channel_id\": \"$MM_CHANNEL_DAILY_BRIEF\", \"message\": \"...\"}"
|
||||
```
|
||||
|
||||
**4. Интерактивный Eagle**
|
||||
|
||||
Опция A: `hermes gateway start` с MM адаптером (+ `MATTERMOST_TOKEN=<eagle token>`)
|
||||
Опция B: ~200 строк `aiohttp` WebSocket демон (самодостаточно, без Hermes)
|
||||
|
||||
**5. История Zulip (опционально)**
|
||||
|
||||
Прямого инструмента Zulip→MM нет. Варианты:
|
||||
- Написать конвертер: Zulip JSON export → MM bulk import JSONL
|
||||
- Кодировать Topics как префикс сообщения: `**[topic]** текст`
|
||||
- Или просто начать с чистого листа (если история некритична)
|
||||
|
||||
---
|
||||
|
||||
## Статус
|
||||
|
||||
- [ ] **IT Ops**: проверить доступ к MM Integrations → запросить создание ботов
|
||||
- [ ] Получить токены для eagle-bot, executor-bot, brief-bot
|
||||
- [ ] Создать категории + каналы в MM
|
||||
- [ ] Настроить `.env` с токенами
|
||||
- [ ] Выбрать архитектуру (Hermes vs custom scripts)
|
||||
- [ ] Подключить scheduled delivery (brief, inbox, executor)
|
||||
- [ ] Подключить интерактивного Eagle
|
||||
- [ ] Решить вопрос с историей Zulip (мигрировать или нет)
|
||||
@@ -33,14 +33,20 @@ A/B experiment adding a tracker-blocking demo step to onboarding for users who p
|
||||
|
||||
### PR #4544 — main chat-path branch
|
||||
|
||||
- [ ] **Danger CI**: Replace `@UserDefaultsWrapper(key: .daxChatPathVisitSiteSeen)` with `KeyedStoring` protocol pattern — new `@UserDefaultsWrapper` definitions are blocked by Danger rule.
|
||||
- [ ] **Mock refactor** (alessandroboron): `MockDaxDialogsSettings.chatPathPhase` should expose `var _chatPathPhase: DaxDialogs.ChatPathPhase = .visitSite` for direct test control, not replicate real computed logic.
|
||||
- [ ] **Sync restore exclusion**: Exclude users who selected "Restore My Stuff" (sync restore) in `OnboardingIntroViewModel` — Dax dialogs are disabled for those users, breaking the flow.
|
||||
- [ ] **Pixels**: Coordinate with rachelmcr (PR #4687) — may be able to use `variant` parameter on existing pixels instead of adding new chat-path-specific pixel events.
|
||||
- [x] **Danger CI**: `@UserDefaultsWrapper(key: .daxChatPathVisitSiteSeen)` — key is only defined as an enum case; no `@UserDefaultsWrapper` usage remains. Already resolved.
|
||||
- [x] **Mock refactor** (alessandroboron): `MockDaxDialogsSettings.chatPathPhase` is already a plain stored property (`var chatPathPhase: DaxDialogs.ChatPathPhase = .none`), directly settable in tests. Already resolved.
|
||||
- [ ] **Sync restore exclusion**: Exclude users who selected "Restore My Stuff" (sync restore) from experiment enrollment.
|
||||
- **Why it breaks**: When sync data is restored, the synced `homeScreenMessagesSeen > 0` value from the user's prior device triggers `DaxDialogsOnboardingMigrator.migrateFromOldToNewOnboarding()` to set `isDismissed = true`, which kills all contextual Dax dialogs (including the chat-path visit-site and trackers-blocked steps).
|
||||
- **Current state**: The returning-user guard in `insertExperimentStepIfNeeded()` (`OnboardingIntroViewModel.swift:451`) is commented out: `// guard case .introDialog(isReturningUser: false) = introSteps.first,`. Sync-restore users have `isReturningUser: true` (via `OnboardingManager.stepsForCurrentFlow()`) so uncommenting this guard is sufficient to exclude them.
|
||||
- **Action**: In `OnboardingIntroViewModel.insertExperimentStepIfNeeded()`, uncomment the line `// guard case .introDialog(isReturningUser: false) = introSteps.first,`. This excludes all returning users (sync restore + returning installs) from the experiment.
|
||||
- [ ] **Pixels**: Coordinate with rachelmcr (PR [#4687](https://github.com/duckduckgo/apple-browsers/pull/4687)).
|
||||
- **Current state**: Two new pixel events `m_onboarding_chat-path_try-visit-site_unique` and `m_onboarding_chat-path_trackers-blocked_unique` were added in `PixelEvent.swift` but have no `.json5` definition in `iOS/PixelDefinitions/`.
|
||||
- **Preferred approach**: PR #4687 adds a `variant` parameter to existing onboarding pixels. If it merges first, the chat-path steps can use the existing pixels with `variant: "chat-path"` instead of new event names — remove the two new pixel event definitions from `PixelEvent.swift` and use the variant approach.
|
||||
- **Fallback**: If #4687 doesn't merge in time, add entries for both new pixels to `iOS/PixelDefinitions/pixels/definitions/onboarding.json5`.
|
||||
|
||||
### PR #4664 — chat-path dialog polish
|
||||
|
||||
- [ ] **Title scope** (alessandroboron): `RebrandedNewTabDaxDialogFactory` title change ("Next, try visiting a site!") must only apply to chat-path flow, not all onboarding flows.
|
||||
- [x] **Title scope** (alessandroboron): Fixed in commit `c15122a1b0` — `RebrandedNewTabDaxDialogFactory.createSubsequentDialog` now correctly uses `isChatPath ? onboardingTryASiteTitle : onboardingTryASiteNTPTitle`. Already resolved.
|
||||
|
||||
### PR #4668 — UTI (Unified Toggle Input) onboarding flow fixes
|
||||
|
||||
@@ -52,7 +58,9 @@ Fixes to make the chat-path onboarding dialogs work correctly when the Unified T
|
||||
- [x] "You've got this" completion dialog path in UTI mode: `showDuckAIOnboardingCompletionDialogInUTI`.
|
||||
- [x] UTI bar staying active after completion dismiss: call `dismissUnifiedToggleInputToOmnibar` directly.
|
||||
- [x] Stale browsing-dialog flags: `setAsChatFirstPath` now resets all browsing-dialog flags to prevent `chatPathPhase` jumping to `.trackerToEOJ` from stale state.
|
||||
- [ ] **Hardcoded cohort override** (aataraxiaa): `FeatureFlag.swift` has a hardcoded test override — must revert before merge.
|
||||
- [ ] **Hardcoded UTI override** (aataraxiaa): `FeatureFlag.swift` `.unifiedToggleInput` case has `defaultValue: .enabled` hardcoded (forces UTI on for testing). The original config is commented out on the next line.
|
||||
- **Action**: Revert to `Config(source: .remoteReleasable(.subfeature(AIChatSubfeature.unifiedToggleInput)))` — remove `defaultValue: .enabled`.
|
||||
- **Note**: `OnboardingIntroViewModel.resolveDuckAIQueryExperimentCohortID()` also has `return .treatmentA` hardcoded before the real flag logic. Both must be reverted before merge (the `resolveDuckAIQueryExperimentCohortID` fix is tracked under PR #4544 returning-user guard work above).
|
||||
|
||||
### PR #4591 — ui-polish
|
||||
|
||||
@@ -60,10 +68,17 @@ Fixes to make the chat-path onboarding dialogs work correctly when the Unified T
|
||||
|
||||
### PR #4855 — sr-feedback
|
||||
|
||||
- [ ] **Hide toolbar + address bar**: During visit-site step, hide both toolbar and address bar via `chromeDelegate?.setBarsHidden(true/false)` — not just lock controls. Costas + Alessandro preference. Simplifies the code too.
|
||||
- [ ] **Use delegate**: `NewTabPageViewController` should access the parent via `NewTabPageControllerDelegate` instead of `(parent as? MainViewController)?` cast.
|
||||
- [ ] **Loading guard**: Move `currentTab?.isLoading != true` check into the `if` condition, not a standalone `guard` (avoids orphaned reference and blank NTP on early exit).
|
||||
- [ ] **Revert Search Experience screen copy**: `searchAndDuckAIOption` was incorrectly changed to `"Toggle between\nSearch and Ask AI"`. Must revert to `"Toggle between\nSearch and Duck.ai"` — the "Ask AI" rename is only for the DuckAIQuery experiment toggle (`DuckAIQueryExperiment.toggleAILabel`).
|
||||
- [ ] **Hide toolbar + address bar**: Replace current approach (separate `setNavigationBarHidden(true)` + `setChatPathVisitSiteControlsLocked(true)`) with `chromeDelegate?.setBarsHidden(true)`.
|
||||
- **Current code**: `NewTabPageViewController.swift:472-478` hides the nav bar and locks toolbar controls separately.
|
||||
- **Requested approach** (Costas + Alessandro): `chromeDelegate?.setBarsHidden(true/false)` hides both address bar and toolbar in one call, which is simpler and matches how other parts of the app hide chrome.
|
||||
- **Action**: In `showNextDaxDialogNew` (the visit-site `spec == .subsequent` block): replace `setNavigationBarHidden(true)` + `setChatPathVisitSiteControlsLocked(true)` with `chromeDelegate?.setBarsHidden(true)`. In `dismiss()` and `dismissHostingController`: replace `setNavigationBarHidden(false)` + `setChatPathVisitSiteControlsLocked(false)` with `chromeDelegate?.setBarsHidden(false)`.
|
||||
- [ ] **Use delegate**: `NewTabPageViewController` casts to `(parent as? MainViewController)?` in several places (e.g. lines 280, 473, 475, 510) to call `setChatPathVisitSiteControlsLocked` and check `currentTab?.isLoading`.
|
||||
- **Action**: Add `func newTabPageControllerWillShowChatPathVisitSiteDialog()` and `func newTabPageControllerDidDismissChatPathVisitSiteDialog()` (or equivalent) to `NewTabPageControllerDelegate`, implement them in `MainViewController`, and route the locking/bar-hiding calls through the delegate instead of casting.
|
||||
- **Note**: Once bars are hidden via `setBarsHidden` (item above), the `setChatPathVisitSiteControlsLocked` call may be removed entirely, simplifying the delegate surface.
|
||||
- [ ] **Loading guard placement**: `NewTabPageViewController.swift:473`: `guard (parent as? MainViewController)?.currentTab?.isLoading != true else { return }` exits early and leaves `didHideBarsForChatPathVisitSiteDialog` unset, resulting in an orphaned lock and a blank NTP if a tab happens to be loading.
|
||||
- **Action**: Fold the condition into the outer `if` block: `if spec == .subsequent, chatPathPhase == .visitSite, (parent as? MainViewController)?.currentTab?.isLoading != true { ... }` so the whole block (including `didHideBarsForChatPathVisitSiteDialog = true`) is skipped atomically.
|
||||
- [ ] **Revert Search Experience screen copy**: `UserText.swift:2374` `searchAndDuckAIOption` value is currently `"Toggle between\nSearch and Ask AI"` — needs reverting to `"Toggle between\nSearch and Duck.ai"`.
|
||||
- **Why**: The "Ask AI" rename (GJ8) applies only to the DuckAIQuery experiment toggle, which now uses the dedicated `DuckAIQueryExperiment.toggleAILabel = "Ask.ai"` string. The Search Experience screen in standard onboarding must keep "Duck.ai".
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user