msg.Collect()) во время синхронизации писем. Реализована настоящая io.Reader потоковая передача, которая записывает необработанные IMAP-потоки непосредственно в локальный .eml временный файл. Это обеспечивает O(1) потребление памяти на папку независимо от размеров писем и вложений.MinConns=5 и ограничения MaxConns (по умолчанию 100) для предотвращения тайм-аутов отключения подключений при высокой нагрузке.SyncWorker. Группировка нескольких UID сообщений в одну IMAP FETCH команду для снижения задержки, и реализована ограниченная errgroup (макс. 4 одновременных парсера ЦП на рабочий) для ускорения разбора MIME и предотвращения длительных блокирующих циклов ЦП.syncPool (5 подключений) с помощью poolctx.WithSync(ctx), гарантируя, что тяжелые задания синхронизации никогда не исчерпают основной пул подключений API (20 подключений).panic: pattern "/api/health" conflicts при запуске путем удаления дублирующихся регистраций маршрутов в ServeMux.ResetAccountSync для правильной очистки осиротевших физических файлов (.eml и вложений) с диска, а не просто очистки базы данных, предотвращая бесконечное раздувание хранилища.PRAGMA busy_timeout=30000; на подключениях SQLite для смягчения database is locked (SQLITE_BUSY) ошибок при сильной конкуренции чтения/записи в Mono edition.Peek: true в IMAP FetchOptions спецификаторе раздела тела.align, valign, bgcolor) и фонов тел путем рефакторинга политики очистки HTML и сохранения <body> тега как суррогатного <div>.new-email событий, предотвращая засорение бэкенда начальными синхронизациями IMAP и срабатывание глобальных блокировок по ограничению скорости IP./api/users, /api/groups).SyncAllFolders явный вызов во время runSyncCycle, исправляя проблему, при которой недавно подключенные или сброшенные учетные записи не загружали исторические письма до ручного вмешательства.*server* из .gitignore для разблокировки go vet и удаленных сборок, которые не удавались из-за отсутствия internal/async/server.go.OFFSET и вложенные OR условия для пагинации писем. Реализовано строгое сравнение кортежей (is_pinned, date_sent, id) < ($1, $2, $3) для задержки запроса O(1) на любой глубине, и добавлен выделенный покрывающий индекс idx_emails_pagination в schema.sql.(folder_id, is_read, is_muted) для устранения последовательных сканирований, когда фоновый рабочий процесс пересчитывает количество непрочитанных сообщений в папке каждые 30 секунд.idx_emails_folder_isread индекс в schema_mono.sql для устранения полных сканирований таблиц и сценариев зависания базы данных во время RefreshUnreadCounts фоновых задач.IMAP-сервер (например, Gmail) автоматически помечал новые отправленные письма как \Seen на стороне сервера, потому что наш загрузчик запрашивал тело письма без параметра Peek параметра, из-за чего они мгновенно отображались как "прочитанные" в интерфейсе.
Исправление (worker.go):
Peek: true в FetchItemBodySection в синхронизирующем рабочем процессе, чтобы предотвратить изменение IMAP-запросами флага \Seen флага.Запуск "Reset Account Sync" удалял строки базы данных для писем и вложений, но оставлял физические .html и файлы вложений осиротевшими на диске, вызывая бесконечное раздувание хранилища.
Исправление (storage.go для Postgres и SQLite):
ResetAccountSync. Теперь бэкенд явно запрашивает и перебирает все body_path и вложения path строки и вызывает os.Remove() перед удалением строк базы данных.Открытие цепочки писем или отображение списка входящих сообщений испытывало значительные задержки (до 10 секунд) из-за отсутствия покрывающих индексов и неоптимальных поисков по очереди.
Исправление (schema.sql, storage.go):
idx_emails_thread, idx_emails_unread_fast, и idx_emails_folder_isread для ускорения массовых COUNT(*) и запросов цепочек.idx_sync_queue_fetch_active для приоритизации account_id в качестве основного префикса, спасая рабочий процесс синхронизации от сканирований таблиц.Письма, отформатированные с помощью устаревших HTML-таблиц (align, valign, bgcolor) и фоны body потеряли все структурное оформление и цвета, потому что bluemonday агрессивно удалял <body> тег и устаревшие атрибуты оформления.
Исправление (email_handlers.go, email_normalize.go):
align, valign, и bgcolor атрибуты глобально по всем table, tr, td, и th тегам в bluemonday политике.normalizeEmailHTML для динамического переименования верхнего уровня <body> тега в <div id="rms-mail-body-surrogate"> перед санитизацией, обходя bluemonday поведение по удалению тега body, при этом безопасно сохраняя все встроенные стили и фон.Тяжелые фоновые задачи, в частности синхронизация IMAP и количество непрочитанных папок, вызывали у SQLite ошибку database is locked (5) (SQLITE_BUSY) и блокировали API, Webhook poller и другие рабочие процессы.
Исправление (schema_mono.sql, storage.go):
idx_emails_folder_isread в schema_mono.sql что мгновенно устранило сканирование таблиц на 10+ секунд во время RefreshUnreadCounts фоновой задачи.PRAGMA busy_timeout=30000; при инициализации драйвера LibSQL, что указывает SQLite ставить в очередь конкурентные блокировки записи вместо мгновенного отказа.Боковая панель фронтенда отображала группы проектов, даже если они не содержали учетных записей (например, после удаления учетных записей из группы).
Исправление (email-sidebar.tsx & models/email.go):
accounts_count в модель ProjectGroup бэкенда.accounts_count == 0.При входе в новую учетную запись начальная IMAP-синхронизация бэкенда отправляла SSE-событие new-email для каждого загруженного письма. Фронтенд useEmails.ts мгновенно реагировал на каждое событие, инвалидируя запросы, что вызывало тысячи параллельных API-запросов, достигавших бэкенда InMemoryRateLimiter лимит (300 запросов/минуту), блокируя IP пользователя глобально по всем /api/* конечным точкам.
Исправление (useEmails.ts):
setTimeout дебанс.Недавно подключенные IMAP-учетные записи (или учетные записи, сброшенные вручную) полагались исключительно на механизм push-уведомлений IMAP IDLE или периодические задания для получения исторических писем. Они не выполняли полный обход папок активно.
Исправление (worker.go):
SyncAllFolders(ctx, c, acc) в runSyncCycle() чтобы оба Mono и Unified издания надежно выполняли полную нисходящую синхронизацию всех папок во время основного цикла синхронизации. go vet и удаленные сборки CI завершались с ошибками ссылок "undefined" для сервера фоновых заданий.
Исправление (.gitignore):
*server* из .gitignore исключений, чтобы обеспечить internal/async/server.go правильно фиксируется в репозитории.В Mono (или whitelabel) издании бэкенд намеренно не подключает /api/users и /api/groups конечные точки. React Query во фронтенде получал 404 Not Found для этих конечных точек и входил в агрессивный цикл повторных попыток с экспоненциальной задержкой, создавая ненужный сетевой шум и способствуя ограничению скорости.
Исправление (useEmailQueries.ts):
enabled проверки включения typeof window !== "undefined" && localStorage.getItem("geomail_edition") !== "mono" && !window.location.host.startsWith("wm.") ко всем административным запросам, чтобы они просто пропускали загрузку при работе в автономной среде.Gmail IMAP возвращает BODY[HEADER] и BODY[TEXT] в произвольном порядке. Старый код брал только первый раздел (break после одной итерации), передавая либо голые заголовки, либо голый текст тела в enmime.ReadEnvelope — вызывая некорректная начальная строка MIME-заголовка на каждом электронном письме, синхронизированном через очередь потребителя.
Исправление (fetcher.go — ProcessMessage + ProcessMessageToFolder):
Specifier и переупорядочены: HEADER всегда первый, затем \r\n разделитель, затем TEXT. Формирует полное сообщение RFC822.repairMIMEBoundaries() регулярное выражение исправляет пропущенные CRLF в Gmail между MIME-границами и следующим заголовком ( --abc123Content-Type: → --abc123\r\nContent-Type:). Соответствует шестнадцатеричным, буквенно-цифровым и _/+= границам.CheckWorker.runSession() создавал временный SyncWorker на каждую попытку. Когда refreshToken() выполнялся успешно, он обновлял только локальную копию Account временного worker'а — CheckWorker's Account оставался устаревшим. Каждый повтор создавал новый SyncWorker из устаревшей копии → бесконечный AUTHENTICATIONFAILED цикл.
Исправление (checker.go, worker.go, manager.go):
"token refreshed, need reconnect" ошибки.Manager.LockTokenRefresh(accountID) — мьютекс для каждого аккаунта сериализует обновления OAuth-токенов. Только одна горутина вызывает Google; остальные ждут и продолжают со свежими токенами.refreshToken() в SyncWorker захватывает блокировку для аккаунта перед обновлением.EnqueueUIDs (ON CONFLICT (account_id, folder_name, uid)) и SaveEmailToFolder (ON CONFLICT (msg_id, account_id, folder_id)) требуют уникальных индексов, которые могут отсутствовать в рабочих базах данных после повторной синхронизации/перестроения.
Исправление (sync_queue.go, storage.go):
EnqueueUIDs перехватывает 42P10 → откатывается к INSERT ... WHERE NOT EXISTS.SaveEmail / SaveEmailToFolder перехватывают 42P10 → откатываются к проверке, затем вставке или обновлению.emails_msg_id_account_folder_key уникальный индекс (3 столбца).Коррелированный COUNT(*) подзапрос с вложенным smart-category NOT IN выполнялся на каждую строку папки. При 50 000 писем × 10 папок = минуты.
Исправление (storage.go, schema.sql, queue_manager.go):
folders.unread_count INT DEFAULT 0 столбец.GetFolders теперь читает COALESCE(f.unread_count, 0) напрямую (субмиллисекундно).RefreshUnreadCounts() выполняется каждые 30 секунд — один пакетный UPDATE для всех папок, а не для каждой отдельно.idx_emails_folder_unread для быстрого подсчета.e.msg_id NOT IN (SELECT e2.msg_id ... WHERE e2.account_id = e.account_id ...) — коррелированный подзапрос, выполняемый для каждой строки письма.
Исправление (storage.go, schema.sql):
emails.smart_category BOOLEAN DEFAULT false столбец.RefreshUnreadCounts помечает письма Promotions/Social/Updates в фоновом режиме.GetEmails фильтрует по простому булевому значению: e.smart_category = false вместо коррелированного NOT IN.idx_emails_smart_cat для быстрой фильтрации.Вебхук-поллер и менеджер очереди столкнулись с база данных заблокирована и повторяли попытки с фиксированными интервалами, что вызывало высокую загрузку ЦП.
Исправление (webhook_queue_mono.go, queue_manager.go):
SQLITE_BUSY / база данных заблокирована ошибках.Отдельные SELECT COUNT(*) для каждой строки агента вместо агрегации в основном запросе.
Исправление (sqlite/storage.go):
SUM(CASE WHEN e.is_read = 0 THEN 1 ELSE 0 END) в GROUP BY — один запрос.misc_handlers.go:491-493). Ответы об ошибках OAuth больше не включают полное тело HTTP (может содержать токены). Удалено логирование имени пользователя DEBUG в authenticate().169.254.169.254, metadata.google.internal) во всех редакциях, включая Mono.StopAll() теперь ожидает завершения рабочих горутин с помощью sync.WaitGroup (тайм-аут 15 с) вместо немедленного возврата.statement_timeout = 30s (переопределяется через PG_STATEMENT_TIMEOUT env). Предотвращает зависание запросов, блокирующих рабочих процессы бесконечно.GetEmails больше не проглатывает json.Marshal ошибки — возвращает 500 вместо пустого 200.HandleLicense и другие обработчики теперь устанавливают Content-Type: application/json.build-and-push-test.sh + docker-compose-u-test.yml / docker-compose-m-test.yml для изолированных развертываний staging с :test тегами образов.SYNC_MAX_WORKERS, PG_SYNC_MAX_CONNS переменные окружения.GET /api/health возвращает {"status":"ok"} для проверки здоровья Docker. GET /metrics предоставляет 16 счетчиков Prometheus (rms_mail_emails_synced_total, http_requests_total, и т.д.) через api.MetricsHandler.asynq с автоматическими повторными попытками, экспоненциальной задержкой и приоритетами очереди (critical:6, default:3, low:1). internal/async/ пакет с TaskClient + TaskServer.ncruces/go-sqlite3 на tursodatabase/libsql-client-go. Чистый Go WASM драйвер, CGO_ENABLED=0, однописательское соединение (MaxOpenConns=1), режим WAL, busy_timeout=30000. Ноль внешних зависимостей — только локальный файл.saveEmailFallback, saveEmailToFolderFallback) теперь используют ON CONFLICT DO NOTHING для предотвращения состояний гонки при одновременных сохранениях. Регулярное выражение для границ MIME расширено для поддержки - символа в границах.runSyncCycle теперь вызывает SyncAllFolders для каждого соединения — новые учетные записи получают все папки обнаруженными и синхронизированными немедленно (ранее только INBOX, пока не вмешивался CheckWorker).ListFolders больше не пропускает родительские папки, имеющие дочерние — письма во вложенных папках больше не теряются незаметно.SendEmail обработчик теперь использует очередь задач Asynq (EnqueueSendEmailDelayed) когда Redis доступен, с резервным планировщиком. Полный конвейер отправки через HandleSendEmail обратный вызов./mon/ (только Unified, требуется Redis) для проверки очереди задач.worker.go, manager.go). Утечка JWT в логах MCP исправлена (r.URL.Path вместо r.URL.String()). CREATE/DELETE для Identity теперь требуют CheckAccountAccess. Ответы ошибок OAuth очищены от полного тела HTTP. Общий http.Client для отправки вебхуков с правильным освобождением тела.JWTAuthMiddleware. Моно-редакция — безопасная заглушка (nil Redis).webhook_queue.go и job_worker.go отправляли СЫРОЙ СЕКРЕТ в заголовке X-Signature-256 — теперь вычисляется HMAC-SHA256(secret, payload). Секрет никогда не покидает сервер.encryptPassword теперь создает по доменам ключи AES через SHA256(raw || ":" || domain) — пароли IMAP, токены OAuth, ключи MCP и токены Telegram каждый использует независимый ключевой материал (imap_password, oauth_token, mcp_key, telegram_token). Расшифровка использует исходный ключ для устаревших данных.sessionAccountID — MCP-клиент, привязанный к учетной записи A, не может получать new-email события от учетной записи B. SSE (обычный) уже имел CheckAccountAccess.?token= на маршрутах без SSE. Оба Authorization: Bearer и ?token=принято; фронтенд уже использует заголовки.MaxOpenConns увеличено с 1 до 25, MaxIdleConns с 1 до 5. Режим WAL теперь действительно распараллеливает: читатели не стоят в очереди за писателями. DSN включает _synchronous=NORMAL, _cache_size=-64000, _foreign_keys=ON так что каждое соединение из пула получает их. Результат: редакция M обрабатывает 20-30 одновременных пользователей без очередей.maybeRotateWorkers вытесняет самого старого worker каждые 5 минут, когда все maxWorkers слоты заняты. bootstrapMissingWorkers заполняет освободившиеся слоты из очереди ожидающих. Предотвращает бесконечное голодание других из-за одной тяжелой учетной записи.maxWorkers По умолчанию: Увеличено с 10 до 50. Новые учетные записи (created_at DESC) получают приоритет при начальной синхронизации.waitWithTimeout goroutine теперь учитывает отмену родительского контекста — не блокируется при отправке, если вызывающий код покинул канал. Осведомленность о контексте на уровне IMAP остается улучшением на будущее.recoverFromRedis/recoverFromDB goroutines теперь запускаются через явный Start() вызов после SetStore/SetContext, что устраняет окно гонки, в котором они опрашивали с nil-зависимостями.CloseIdleConnections: Добавлена отложенная очистка для каждого вызова http.Client в диспетчерах вебхуков для предотвращения накопления сокетов под нагрузкой.CASE WHEN UPPER(name) = 'INBOX' THEN 1 ... THEN 3 ELSE 2 END — INBOX всегда первая, пользовательские метки в алфавитном порядке, Trash/Spam/Junk принудительно внизу. Gmail [Gmail]/Trash и [Gmail]/Spam распознаются. Применяется как к Postgres, так и к SQLite.smart_categories теперь вызывает RefreshUnreadCounts немедленно, вместо ожидания 30-секундного периодического обновления.WakeUpCh Удаление мертвого кода: Канал никогда не имел потребителя. WakeUpAccount теперь вызывает TriggerRefresh().OnNewEmail Деглобализирован: Перемещено из уровня пакета var в Manager.OnNewEmail поле. Проведено через Fetcher структуру.camoCache LRU: Заменено неограниченное sync.Map на container/list-основанный LRU (10K записей, периодическая очистка при 80% заполнения).stripHTMLTagsFast Декодирование сущностей: Постобработка с помощью strings.NewReplacer для &, <, >, ", ', — улучшает качество сводки AI.slogWriter Уровень: Отладочный трассировщик IMAP теперь использует slog.Debug вместо slog.Info чтобы избежать спама в логах на уровне по умолчанию.notification.RateLimiter Сброс сообщения: Теперь логирует глубину очереди (%d pending) при сбросе.ProcessAutoDraftJob Устойчивость: context.Background() заменен на правильный ctx; игнорируемая ошибка в GetAccount теперь логируется; AppendToDraftsDeduplicated является синхронным (ошибка → повтор задачи вместо тихой потери).forceRestartWorkers TOCTOU: Добавлен LockChecker вызов перед запуском каждого воркера — аккаунт, заблокированный между GetAccounts и запуском воркера, теперь пропускается.ProcessMessage и ProcessMessageToFolder теперь os.Remove(bodyPath) когда SaveEmail завершается неудачей после того, как файл EML уже был записан.generateAIDraft Asynq Wiring: HandleGenerateAIDraft теперь делегирует OnGenerateAIDraft обратный вызов → ProcessAutoDraftJob — Этап 3 работает.sendWebhookWithRetry теперь ставит в очередь через AsyncClient.EnqueueDispatchWebhook когда доступен Redis. Webhook'и получают постоянную очередь, автоматические повторные попытки (5x), и asynqmon видимость панели управления. Возврат к Redis ZSET (Unified без Asynq) → SQLite (Mono).handleAutoDraft теперь предпочитает EnqueueGenerateAIDraft вместо очереди заданий БД, когда доступен AsyncClient. Возвращается к очереди БД для Mono.ResetAccountSync Полная очистка: Очистка email_sync_queue, imap_move_queue, scheduled_emails, email_comments, и emails_fts (SQLite) при сбросе аккаунта. Ранее, email_sync_queue сохраняла completed задачи, которые блокировали повторную постановку в очередь через WHERE status != 'completed' защиту в EnqueueUIDs — что приводило к 0 писем после повторной синхронизации. Теперь все состояние синхронизации полностью очищается, так что воркер выполняет чистую полную повторную синхронизацию.OnNewEmail и SetEventBroadcast обратные вызовы больше не вызывают store.GetEmail на основном пуле БД. Ранее, каждое синхронизированное письмо вызывало 2 GetEmail запросов к основному пулу (один в OnNewEmail, один в InvalidateEmailCacheByEmailID). С 200K+ почтовыми ящиками Gmail это генерировало 400K запросов, конкурирующих с HTTP-обработчиками, что вызывало 10-минутный паралич API после перезапуска. Теперь OnNewEmail получает subject/senderName/senderAddr напрямую от Fetcher (ноль запросов к БД), и широковещательная инвалидация кэша использует account_id из payload вместо повторной загрузки письма.InvalidateMetaCache при мутациях:GetEmails (5 мин), GetFolders (30 с), GetAccounts (10 с), GetGroups (30 с)GetLabels (60 с), GetRules (30 с), GetTemplates (60 с), GetContacts (30 с)GetIdentities (60 с), GetWebhooks (30 с), AIModels (1 ч)InvalidateMetaCache очищает все кэши для каждого аккаунта при CRUD-операциях. InvalidateEmailCache также очищает кэши папок. Новый InvalidateMetaCache хелпер для массовой инвалидации при мутациях аккаунтов/групп. Снижает нагрузку на БД при каждой загрузке страницы с 4+ запросов до 0 (попадание в кэш) или 1 (промах кэша + запись). Ранее каждое синхронизированное письмо запускало 2 GetEmail запроса к основному пулу (один в OnNewEmail, один в InvalidateEmailCacheByEmailID). С 200K+ почтовыми ящиками Gmail это генерировало 400K запросов, конкурирующих с HTTP-обработчиками, что вызывало 10-минутный паралич API после перезапуска. Теперь OnNewEmail получает subject/senderName/senderAddr напрямую от Fetcher (ноль запросов к БД), и широковещательная инвалидация кэша использует account_id из payload вместо повторной загрузки письма.ResetAccountSync удалил письма из БД, но оставил 6148 completed строк в email_sync_queue. Когда SyncAllFolders повторно поставил UID в очередь, ON CONFLICT ... DO UPDATE ... WHERE status != 'completed' предложение пропустило их все. Потребительский цикл ничего не извлёк → после ресинка появилось 0 писем.
Исправление (postgres/storage.go, sqlite/storage.go):
DELETE FROM email_sync_queue WHERE account_id = $1 добавлено перед сбросом UID папки/аккаунта.imap_move_queue, scheduled_emails, email_comments (подстраховка для таблиц с FK CASCADE).DELETE FROM emails_fts (виртуальная таблица FTS5 не поддерживает FK, что приводило бы к накоплению осиротевших записей индекса).GetEmailsCursor в хранилищах Postgres и SQLite: заменяет LIMIT 50 OFFSET 50000 на составной курсор (date_sent, id) — константное время независимо от глубины страницы.X-Next-Cursor заголовок ответа с Access-Control-Expose-Headers в конфигурации CORS.useEmailsInfinite использует pageParam в качестве строки курсора, читает X-Next-Cursor из заголовков ответа axios.?offset= продолжают работать через оригинальный GetEmails.GetFolders, GetUnreadCountByAccount, GetUnreadInboxCountByAccount, GetUnreadCountByFolder теперь используют прямой COUNT(*) FROM emails WHERE is_read=false вместо кэшированного folders.unread_count столбца. Частичный индекс idx_emails_folder_unread делает их субмиллисекундными.MarkEmailRead, BulkMarkEmailsRead, BulkMarkEmailsUnread обновляют folders.unread_count атомарно через CTE — но авторитетным источником теперь является живой COUNT.RefreshUnreadCounts удалён из queue_manager.go — больше нет 30-секундного фонового таймера, тратящего CPU на простаивающие счётчики.publishEvent включает account_id в сообщениях массовых операций — инвалидация кэша теперь срабатывает корректно для переключений прочитано/непрочитано.MemoryCache с TTL и периодической очисткой. Когда Redis недоступен (Mono), 11 GET-эндпоинтов используют в памяти cacheGet/cacheSet/tryCache хелперы — прозрачное переключение между Redis и локальной памятью.CheckAccountAccess кэширование: Использует cache:account:meta:{id} (30s TTL) чтобы избежать GetAccount запрос к БД на каждый API-вызов. Снижает накладные расходы аутентификации с N+1 до O(1).Get/Set/Ping таймауты: redisOpTimeout=3s применяется ко всем операциям Redis — предотвращает зависание HTTP-воркеров при медленном Redis.InvalidateEmailCache использует переданный ctx вместо context.Background() для правильного управления жизненным циклом.OnSendTelegram гранулярный кэш: cache:account:meta:{id} заменяет полный accounts:list JSON парсинг — поиск O(1) вместо сканирования O(n).publishEvent context.Background() → ctx для инвалидации Redis.MaxConns=100 по умолчанию для пула PostgreSQL (PG_MAX_CONNS переопределяется переменной окружения)._synchronous=NORMAL&_cache_size=-64000&_foreign_keys=ON для всех соединений пула.Shutdown() до syncMgr.StopAll() — позволяет завершить выполняющиеся задачи SMTP/webhook.Asynq.Start(ctx) привязан к контексту приложения вместо context.Background().WithTimeout(1s) — предотвращает утечки goroutine во время завершения работы.case "webhook" из StartJobWorker (мертвый код — вебхуки проходят через опросчик Asynq/Redis ZSET/SQLite).EnqueueRefreshUnread (мертвый асинхронный тип — количество непрочитанных теперь в реальном времени).HandleSendEmail SkipRetry для отмененных задач — прекращает 10-кратный повторный спам.SendScheduler.Start() явный жизненный цикл — горутины запускаются после установки зависимостей.callAIChatWithTools и callAIChat в ai_handlers.go заменили 80 строк мутации общего провайдера на месте на 6-строчную поверхностную копию через ai.OverrideProviderSettings(). Одновременные AI запросы с разными моделями/ключами больше не приводят к состоянию гонки.OverrideProviderSettings экспортирован: gateway.go — переименован из overrideProviderSettings в экспортированный OverrideProviderSettings. Добавлен *OpenCodeProvider случай.resolveAPIKey теперь перебирает ВСЕ ключи из ENCRYPTION_KEYS через новый crypto.GetAllEncryptionKeys() вместо только первого ключа. Настройки AI, зашифрованные старыми ключами после смены, теперь расшифровываются корректно.MemSessionStore потокобезопасный: Добавлен sync.RWMutex защищающий все операции с картой — предотвращает fatal error: concurrent map writes крах при одновременном трафике Telegram.decryptTelegramToken в main.go пробует все доменные ключи (telegram_token) и сырые ключи перед откатом к шифротексту — исправляет ошибку аутентификации бота, когда токен загружается из БД.UpsertAISettings теперь возвращает HTTP 500, когда ENCRYPTION_KEY не настроен, но API-ключи сохраняются — предотвращает незаметное хранение в открытом виде.api_keys_encrypted → api_keys: Переименовано JSON поле в передаче (фронтенд ↔ бэкенд). Имя столбца БД не изменено.AICategorizeEmail разбор: Заменен strings.Split на ai.ParseCategories — корректно обрабатывает маркированные списки и многострочные ответы AI.GetWebhooks больше не возвращает Secret поле на фронтенд.GetTelegramSettings возвращает XXXX...XXXX вместо реального токена.err.Error() == "no rows in result set" → идиоматический errors.Is(err, pgx.ErrNoRows) во всех 5 местах.mixtral-8x7b → mixtral-8x7b-32768 в ProviderModels.callAIChat для Ollama теперь передает effectiveKey вместо хардкодного "".EnqueueWebhook теперь записывает в лог, когда Redis недоступен.refetch() вызов при обновлении — вызывал тройной повторный рендеринг.FetchAnthropicModels (Anthropic API) и FetchOpenAICompatModels (OpenCode) в fetchProviderModels. Теперь все провайдеры получают актуальные модели из своих API вместо жестко заданных списков. Жестко заданный ProviderModels обновлен до актуальных моделей 2026 года для всех 10 провайдеров.FetchGeminiModels теперь фильтрует только gemini-* модели, исключая устаревшие модели PaLM (chat-bison, text-bison, embedding, aqa).FetchGeminiModels и FetchOllamaModels — ранее молча проглатывал ошибки API.ChatWithTools теперь использует BaseURL вместо жестко заданного api.opencode.com. ListModels был return nil, nil — теперь получает от провайдера. Добавлен ProviderEnvKey "opencode" case. Обнаружение облачного URL (/zen/go/v1 для opencode.ai, /v1/models для локального).callAPI и callAPIWithTools переписаны в правильный формат Gemini: system → systemInstruction (отдельное поле), ассистент → модель сопоставление ролей. Ранее все сообщения сжимались в один текстовый блок.UpsertAISettings теперь объединяет входящие ключи с существующими сохраненными ключами. Ввод ключа одного провайдера больше не удаляет все остальные сохраненные ключи. Расшифровывает существующие, объединяет непустые значения, повторно шифрует.resolveAPIKey глобальный запасной вариант больше не требует ALLOW_GLOBAL_AI_KEYS=true (теперь включено по умолчанию, отключается =false). Удалено setting.Preset == "" блокировщик. Правильный цикл по идентификаторам аккаунтов-кандидатов.fetchProviderModels теперь пробует все ключи из ENCRYPTION_KEYS (через запятую), а не только первый — исправляет получение моделей после ротации ключей.AIChat, summarizeEmail, AICategorize, AICategorizeEmail) теперь возвращают {"error":"..."} JSON вместо стандартного "internal error" — пользователи могут видеть реальные сообщения об ошибках API.callAIChat и callAIChatWithTools больше не перезаписывают модель провайдера пустой строкой, когда apiKey присутствует, но модель пуста.grok-2 в DeepSeek API).InvalidateEmailCache поддержка MemCache: Удален ранний return при Redis равном nil (Mono edition). Теперь корректно инвалидирует как Redis (Unified), так и MemCache (Mono). Добавлен метод MemoryCache.Keys() для инвалидации по префиксу.Del("folders:unified", "folders:") на scanAndDel("folders:*") — правильно очищает все кеши папок для конкретного аккаунта.markEmailRead, toggleFlagEmail, togglePinEmail, toggleMuteEmail, snoozeEmail, saveDraftReply, clearDraftReply, moveEmail, SetEmailLabels теперь вызывают InvalidateEmailCache + publishEvent("email_updated") с правильным account_id.account_id не предоставлен фронтендом, явно вызывает InvalidateEmailCache("") — ранее молча пропускалось.email_updated обработчик событий, который инвалидирует ["email", id], ["emails-infinite"], и ["folders"] кэши React Query — ранее только new-email обрабатывался.selectedEmail ссылка на объект изменялась при каждом повторном запросе. Теперь используется useRef с отслеживанием ID письма — таймер запускается один раз и срабатывает надежно после заданной задержки.__pending__ защита: Предотвращает повторный запуск таймера после успешной мутации, пока кэш React Query еще не обновился. Очищается, когда is_read фактически переключается.["accounts"] инвалидация в useMarkEmailRead: Автоматическая отметка прочитанным инвалидировала ["folders"] но не ["accounts"] — счетчик заголовка боковой панели (accounts.reduce(... a.unread_inbox)) никогда не обновлялся. Ручная кнопка работала, потому что useBulkEmailAction действительно инвалидировала аккаунты.["folders"]: useFlagEmail, usePinEmail, useSnoozeEmail ранее только инвалидировали ["emails-infinite"] — счетчики папок боковой панели устаревали после действий с флагом/закреплением/откладыванием. Добавлен refetchQueries для папок, чтобы обойти staleTime.is_dirty_locally точка удалена с карточек писем — сбивающий с толку UX, пользователям всё равно на статус синхронизации IMAP.useUsers отключен для версий, не относящихся к Teams: Предотвращает спам 404 на /api/users в версиях Unified и Mono.IsTeams() блок — работает на U, M, T.[SSE] connecting, [SSE] connected, [SSE] event source error) из useEmails.ts.hasSavedKey сохранение: Флаг больше не сбрасывается при сохранении без повторного ввода ключей после перегрузки страницы.