Files
glchat/desktop
grendervill d2443ae460 release: glchat 0.1.2
Версия 0.1.1 выпускалась до удаления OAuth: приводим номер в соответствие с
состоянием кода (в 0.1.2 входа через внешние провайдеры уже нет, схема БД — 27).

- VERSION, tauri.conf.json, Cargo.toml, Cargo.lock → 0.1.2;
- состав изменений с 0.1.1: удалён OAuth целиком (D-084), миграция 00027
  убирает таблицу oauth_accounts, зависимость golang.org/x/oauth2 убрана.
2026-09-27 18:49:15 +03:00
..
2026-09-27 18:49:15 +03:00

desktop — desktop-обёртка glchat (Tauri 2)

Тот же веб-клиент из web/, обёрнутый в Tauri 2 (docs/client-tauri.md). Отдельного UI-кода нет: страница инстанса загружается в системный webview, а обёртка даёт платформенную интеграцию — трей, нативные уведомления, глобальный push-to-talk, автозапуск, deep links и автообновление.

desktop/
└── src-tauri/            # Rust-проект обёртки
    ├── src/              # окно, трей, шорткаты, уведомления, deep links, обновления
    ├── capabilities/     # allowlist IPC: локальная страница настроек и домен инстанса
    ├── ui/               # локальная страница настроек обёртки (tauri://localhost)
    ├── icons/            # иконки приложения (сгенерированы из web/public/icons)
    └── tauri.conf.json   # конфигурация сборки и плагинов

Сборка и запуск

make desktop-check                 # cargo fmt --check, clippy, тесты (кэш в .cache/)
make desktop-build                 # бандлы текущей ОС (macOS: .app и .dmg)
make desktop-build-app             # только .app + артефакты обновления, без .dmg
open desktop/src-tauri/target/release/bundle/macos/glchat.app

Требуется Rust 1.77+ и Node 22 (Tauri CLI ставится через npx). Кэши cargo и npm держатся в .cache/ — как у остальных инструментов проекта.

make desktop-build-app (tauri build --bundles app) нужен там, где .dmg собрать нельзя: bundle_dmg.sh монтирует образ через hdiutil и управляет Finder через AppleScript, поэтому из песочницы агента и из headless-окружения он падает (см. «Что не сделано»).

open --args из песочницы агента аргументы не доставляет (проверено на macOS 27: и на бандле glchat, и на системном TextEdit — процесс запускается без них; из обычного терминала то же самое работает). Поэтому make desktop-run ARGS='--minimized' и замер свёрнутого окна в scripts/desktop-perf.py надо запускать из обычного терминала, а не из песочницы.

Сборка релиза с артефактами автообновления использует ключ подписи:

# ключ и пароль не коммитятся: приватный ключ живёт в CI-секретах,
# локально — в build/desktop-keys/ (каталог build/ в .gitignore)
npx @tauri-apps/cli@^2 signer generate -w build/desktop-keys/glchat-updater.key -p '…'
make desktop-build

Публичный ключ уже вписан в src-tauri/tauri.conf.json и в UPDATER_PUBKEY (src/updates.rs). Без локального ключа make desktop-build собирает бандл без артефактов обновления, чтобы сборка не падала у тех, у кого ключа нет.

Аргументы командной строки

Аргумент Что делает
--instance <url> адрес инстанса вместо сохранённого (--instance=https://… тоже работает)
--minimized запуск свёрнутым в трей (используется автозапуском)
--settings сразу открыть страницу настроек обёртки
--autostart on|off включить или выключить автозапуск при установке скриптом
--update-now сразу проверить обновление и поставить его без диалога (тихое обновление)

Настройки

Хранятся в settings.json каталога конфигурации приложения (~/Library/Application Support/su.mhspx.glchat/ на macOS):

Ключ Смысл
instanceUrl адрес клиента, по умолчанию https://gl.mhspx.su
closeToTray закрытие окна сворачивает приложение в трей
autostart запуск при входе в систему (--minimized)
notifications показывать системные уведомления
pttShortcut, muteShortcut, deafenShortcut глобальные комбинации (пусто — выключено)

Меняются из меню трея и со страницы настроек обёртки (трей → «Настройки обёртки…»). Сессия и токены живут только в cookie webview: обёртка их не хранит.

Схема glchat:// (регистрируется бандлом):

  • glchat://invite/<code> → страница приглашения;
  • glchat://guild/<guildId>/channel/<channelId> → сервер и комната.

Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и доставляется после desktop_ready.

Автообновление

Обёртка проверяет обновления сама: фоновая проверка через 30 секунд после старта и вручную — трей → «Проверить обновления…». Адрес манифеста — GET {instance}/updates/{target}/{arch}/{current_version} (target: darwin/windows/linux, arch: aarch64/x86_64; см. src/updates.rs).

Найденное обновление показывается в интерфейсе, установка спрашивается отдельным диалогом («во время звонка перезапуск без согласия недопустим»). Для управляемых машин есть тихий режим — флаг --update-now:

open -a /Applications/glchat.app --args --update-now   # из обычного терминала

С этим флагом обёртка проверяет обновление сразу при старте (не через 30 секунд) и ставит найденное без вопросов, после чего перезапускается; диалогов нет вовсе, а если обновления нет — приложение просто работает дальше. По умолчанию флаг выключен, поведение обычного запуска не меняется. Подпись артефакта проверяется в обоих режимах: флаг убирает только вопрос пользователю, но не проверку (docs/DECISIONS.md, D-077).

В журнале (~/Library/Logs/su.mhspx.glchat/glchat.log) видны и версия запущенной сборки, и ход обновления:

обёртка запущена: версия=0.1.0, адрес=https://gl.mhspx.su, окно=видимо, автозапуск=true
тихое обновление по флагу --update-now: проверка сразу при старте
доступно обновление 0.1.1
тихое обновление по флагу: 0.1.0 → 0.1.1
обновление 0.1.1 установлено, перезапуск
обёртка запущена: версия=0.1.1, …

Манифест отдаёт сам инстанс (internal/server/updates.go), отдельная статика в Caddy не нужна: и встроенный Caddy профиля, и внешний прокси стенда проксируют весь домен на приложение, поэтому /updates/... доходит до него без правок чужих конфигов.

Раскладка каталога (UPDATES_DIR; на стенде /opt/glchat/updates, в контейнере смонтирован как /app/updates:ro):

/opt/glchat/updates/
└── darwin/aarch64/
    ├── glchat.app.tar.gz         # артефакт релиза (создаёт tauri build)
    ├── glchat.app.tar.gz.sig     # подпись ed25519 (формат minisign)
    ├── 0.1.1.json                # манифест текущего релиза
    └── latest.json               # манифест последнего релиза — его и отдаёт инстанс

Манифестов прошлых релизов в каталоге быть не должно: обёртка спрашивает /updates/{target}/{arch}/{своя версия}, и оставшийся <старая версия>.json перекрыл бы новый релиз — клиент со старой версией получил бы «обновлений нет». Публикующий скрипт сам убирает такие файлы (см. ниже).

Выпуск релиза

Версия живёт в одном месте — файл VERSION в корне репозитория; из него её берут Makefile (версия сервера и образов), deploy/install.sh (IMAGE_TAG) и desktop-обёртка (desktop/src-tauri/tauri.conf.json + Cargo.toml — их значения должны совпадать с VERSION).

  1. Поднять версию:

    echo 0.1.2 > VERSION                       # версия сервера и обёртки
    # те же значения — в desktop/src-tauri/tauri.conf.json, Cargo.toml, Cargo.lock
    make web-build && make build               # сервер собирается с новой версией
    
  2. Проверки и сборка бандла:

    make go-lint && go test -tags sqlite_fts5 -race -count=1 ./internal/...
    cd web && npm run check && npm run lint && npm test && cd ..
    make desktop-check                         # fmt, clippy, cargo test
    make desktop-build                         # .app + .dmg + артефакты обновления
    
  3. Зафиксировать и пометить релиз:

    git add VERSION Makefile deploy/install.sh desktop/src-tauri scripts/desktop-release.sh
    git commit -m "release: 0.1.2"
    git tag -a v0.1.2 -m "glchat 0.1.2" && git push origin main --tags
    
  4. Опубликовать обновление на инстансе:

    sudo make desktop-release                                   # локальный инстанс
    make desktop-release-upload HOST=user@server                # по SSH (sudo спросит пароль)
    make desktop-release-upload HOST=user@server \
      GLCHAT_SSH_OPTS="-i ~/.ssh/key -o IdentitiesOnly=yes"     # свой ключ, без TTY (CI)
    make desktop-release ARGS='--dry-run'                       # посмотреть план
    
  5. Проверить канал и живьём обновиться со старой версии — см. следующий раздел.

Скрипт scripts/desktop-release.sh берёт версию из tauri.conf.json, сам определяет target/arch по имени артефакта, копирует артефакт и .sig, собирает манифест (version, notes, pub_date, platforms.<target>-<arch>.signature/url), кладёт его и как <version>.json, и как latest.json, а манифесты прошлых релизов убирает. Несколько платформ одной версии (macOS + Windows + Linux) складываются в один манифест: запускайте скрипт по разу на артефакт. Каталог обновлений на инстансе обычно принадлежит root — тогда rsync идёт через sudo; если каталог записываем (стенд), sudo не нужен и загрузка работает без терминала.

latest.json нужен потому, что обёртка приходит со своей установленной версией: «новее ли релиз» решает клиент сравнением semver (tauri-plugin-updater), поэтому на запрос любой версии сервер отдаёт последний манифест, а если есть манифест конкретной версии — его. Без этого обновление не получил бы никто, кроме тех, у кого уже стоит последняя версия.

Владельцу прокси добавлять ничего не нужно: /updates/... уходит в catch-all handle и доходит до приложения (так же, как статика клиента). Вариант «раздавать обновления статикой мимо приложения» требует правила rewrite на latest.json с подстановкой target/arch в Caddy — лишняя работа без выгоды, поэтому не рекомендуется.

Проверка (стенд):

curl -s https://gl.mhspx.su/updates/darwin/aarch64/0.1.0 | jq .   # манифест для старой версии
curl -sI https://gl.mhspx.su/updates/darwin/aarch64/glchat.app.tar.gz | head -3  # 200 + размер
# в логе обёртки (~/Library/Logs/su.mhspx.glchat/glchat.log) —
# «обновлений нет» (манифест разобран) вместо
# «проверка обновлений не удалась: … 404»

Порядок обновления: проверка → уведомление в интерфейсе → скачивание → подтверждение в диалоге (во время звонка перезапуск без согласия не делается) → подпись проверяется до установки, downgrade отклоняется.

Приёмка релиза на живом инстансе (26.09.2026, 0.1.0 → 0.1.1)

Проверено на боевом канале стенда, без локальных заглушек:

  • манифест опубликован scripts/desktop-release.sh --remote, артефакт glchat.app.tar.gz (3 204 953 Б) и подпись лежат в /opt/glchat/updates/darwin/aarch64/;
  • обёртка 0.1.0, запущенная с --update-now, в журнале (UTC) показала: доступно обновление 0.1.1 → тихое обновление по флагу: 0.1.0 → 0.1.1 → обновление 0.1.1 установлено, перезапуск → обёртка запущена: версия=0.1.1 → обновлений нет (17:08:23–17:08:25Z);
  • запрос старой версии GET /updates/darwin/aarch64/0.1.0 отдаёт манифест 0.1.1 — это и есть та точка, где раньше мешал оставшийся 0.1.0.json;
  • /Applications/glchat.app после установки — 0.1.1, ярлык на рабочем столе (~/Desktop/glchat.app) указывает на него.

Важно про место установки. tauri-plugin-updater распаковывает обновление во временный каталог и переносит бандл rename(2). Если приложение лежит на другом томе, чем временный каталог (например, на внешнем диске /Volumes/…, а TMPDIR — на загрузочном), установка падает с установка обновления не удалась: Cross-device link (os error 18), хотя скачивание и проверка подписи проходят. Поэтому приложение для автообновления надо держать на загрузочном томе — так делает .dmg (ставит в /Applications); ярлык на рабочем столе лучше вести на /Applications/glchat.app, а не на сборку в репозитории.

Локальная проверка «поверх старой версии» (без стенда)

Установка обновления проверена локально на настоящем сервере инстанса: сборка 0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка → перезапуск на 0.1.1. Порядок действий (все данные — в каталоге репозитория, стенд не затрагивается):

# 1. артефакт текущей версии и приложение 0.1.0 (с ключом подписи)
make desktop-build-app
# 2. версия 0.1.1 в desktop/src-tauri/tauri.conf.json и Cargo.toml, затем:
make desktop-build-app
# 3. раскладка манифеста в свой каталог, ссылки — на локальный сервер
bash scripts/desktop-release.sh --artifact \
  desktop/src-tauri/target/release/bundle/macos/glchat.app.tar.gz \
  --version 0.1.1 --base-url http://127.0.0.1:8100 --dir /tmp/updates
# 4. настоящий сервер инстанса со своим каталогом обновлений и клиентом
LISTEN_ADDR=127.0.0.1:8099 DATA_DIR=/tmp/glchat-e2e/data WEB_ROOT=$PWD/web/dist \
  UPDATES_DIR=/tmp/updates DOMAIN=127.0.0.1:8099 TLS_ENABLED=false \
  APP_VERSION=0.1.0-local SESSION_PEPPER=… MASTER_KEY=… TOTP_ENCRYPTION_KEY=… \
  ./build/glchat
# 5. запуск 0.1.0 с тихим обновлением (адрес инстанса — локальный)
/Applications/glchat.app/Contents/MacOS/glchat-desktop \
  --instance http://127.0.0.1:8099 --update-now

Что нужно знать, повторяя это:

  • локальный стенд — http, а tauri-plugin-updater по умолчанию требует https («The configured updater endpoint must use a secure protocol»), для проверки сборку делают с временным флагом tauri build --config '{"plugins":{"updater":{"dangerousInsecureTransportProtocol":true}}}'. В релизной сборке этого флага быть не должно: боевой адрес — https://;
  • приложение должно лежать на пути без символических ссылок: macOS-проверка tauri-utils отвергает current_exe() с symlink-компонентом, поэтому /tmp/glchat-e2e/… не годится (/tmp — ссылка на /private/tmp), а /private/tmp/glchat-e2e/… годится. Тот же смысл у требования ставить приложение в /Applications, а не запускать через ссылку;
  • --update-now обязателен: без него установку спрашивают диалогом, а кликнуть в headless-прогоне некому;
  • диагностика в логе: GET /updates/darwin/aarch64/0.1.0 в журнале сервера, затем доступно обновление 0.1.1 → тихое обновление по флагу: 0.1.0 → 0.1.1 → обновление 0.1.1 установлено, перезапуск → строка запуска с версия=0.1.1 и запрос манифеста уже для 0.1.1;
  • проверка подписи проверяется и «от обратного»: если испортить байт в артефакте, установка обязана упасть с The signature verification failed, а версия на диске — остаться прежней.

Уведомления и переход в канал

Решение «показывать ли уведомление» принимает веб-клиент (упоминания и личные сообщения, окно неактивно или комната не открыта), обёртка показывает системное уведомление и обновляет счётчик.

Клик по уведомлению: у tauri-plugin-notification на desktop нет колбэка действия, поэтому реализовано лучшее доступное поведение — уведомление, показанное при неактивном окне, запоминается, и когда приложение активируется в течение 10 секунд (окно получило фокус или macOS прислала Reopen), окно поднимается и открывается комната из уведомления (glchat://guild/<id>/channel/<id>, для личной беседы glchat://dm/<id>).

Ограничение (честно): ОС не сообщает, что активация вызвана именно кликом по уведомлению, поэтому переход сработает и при обычном возврате в приложение в течение этих 10 секунд. Клик по уведомлению старше 10 секунд только поднимает окно. Полноценный колбэк требует своего моста к UNUserNotificationCenter (UNUserNotificationCenterDelegate) — отдельная задача.

Ресурсы и холодный старт

Замеры собраны скриптом scripts/desktop-perf.py (macOS arm64, Mac на M-серии, инстанс gl.mhspx.su через внешний прокси; подробности — docs/PERF.md):

Метрика Значение Бюджет (docs/client-tauri.md §1)
Холодный старт (open → процесс → строка «загружена страница») 773–898 мс (три прогона) ≤ 2 с
Из них: запуск процесса 74–78 мс —
Из них: загрузка страницы клиента 696–821 мс —
RAM, клиент открыт и простаивает (RSS: обёртка + 4 процесса WebKit) 254 МБ (117 + 137) ≤ 350 МБ
RAM, окно свёрнуто в трей (--minimized) 256 МБ ≤ 350 МБ
Физический след (Activity Monitor) 106 МБ —

Наблюдения:

  • свёрнутое в трей окно памяти не освобождает: webview с загруженным клиентом продолжает жить (так работают WKWebView и трей-режим) — экономия была бы только при выгрузке страницы, а это потеря мгновенного возврата в клиент;
  • первый запуск сразу после сборки бандла (холодный кэш файловой системы и страницы) дал 1,26 с — тоже в бюджете;
  • почти вся память — WebKit (страница клиента), сама обёртка занимает ~117 МБ.

Повторить замер:

make desktop-build-app                       # свежий .app (+ артефакты обновления)
python3 scripts/desktop-perf.py --runs 3     # 3 холодных старта + простой в трее

Что не сделано (открытые пункты Фазы 6)

  • Автообновление проверено локально «поверх старой версии» (2026-09-26): 0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка → перезапуск на 0.1.1, подробности и команды — «Локальная проверка» выше. Открытым остаётся публикация реального релиза на стенде (номер версии поднимает оператор) и то, что локальная проверка шла по http с временным флагом dangerousInsecureTransportProtocol — на стенде канал https.
  • Аватары в уведомлениях (thumb/буква) и группировка — сейчас иконка приложения.
  • Захват экрана и микрофон: разрешения macOS выдаёт webview, отдельных экранов-инструкций у обёртки нет.
  • Подпись и нотаризация macOS — сознательно не делаются (решение владельца 26.09.2026): приложение распространяется неподписанным .dmg, карантин снимает конечный пользователь (инструкция ниже). EV/Trusted Signing для Windows — тем же решением не делаем.
  • .dmg: собирается из обычного терминала (Finder/AppleScript для раскладки окна); в песочнице агента hdiutil create и AppleScript запрещены, но образ без оформления окна собирается флагом --sandbox-safe к bundle_dmg.sh (проверено 2026-09-26, см. ниже).
  • Windows/Linux-бандлы: конфигурация кросс-платформенная, но собирались и проверялись только macOS-arm64. Сборки под другие ОС отложены: репозиторий живёт в Gitea на мини-ПК, а windows-latest/ubuntu-24.04-раннеры есть у GitHub Actions, поэтому владелец зеркалит проект в GitHub позже и запускает сборку там. Локальная часть готова и проверена: make desktop-check, scripts/desktop-release.sh --artifact … --target windows|linux (манифест обновления для каждой платформы складывается в общий файл версии).

Что осталось оператору (точные команды)

macOS: .dmg

bundle_dmg.sh создаёт образ через hdiutil create и раскладывает иконки через AppleScript/Finder. Из песочницы агента и из headless-сессии обе операции запрещены (hdiutil create → «Операция не разрешена», AppleScript → -10004). Из обычного терминала на macOS:

cd /Volumes/Samsung/projects/glchat
make desktop-build                 # .app + .dmg + артефакты обновления
ls -lh desktop/src-tauri/target/release/bundle/dmg/

Если терминала с графической сессией нет (CI, headless), тот же образ собирается без оформления окна — тем же скриптом Tauri с флагом --sandbox-safe (он пропускает AppleScript и hdiutil create):

cd /Volumes/Samsung/projects/glchat
make desktop-build-app             # .app и артефакты обновления
rm -rf /tmp/dmg-stage && mkdir -p /tmp/dmg-stage
cp -R desktop/src-tauri/target/release/bundle/macos/glchat.app /tmp/dmg-stage/
cd desktop/src-tauri/target/release/bundle/dmg
bash bundle_dmg.sh --volname glchat --volicon icon.icns \
  --icon glchat.app 180 170 --app-drop-link 480 170 --hide-extension glchat.app \
  --sandbox-safe glchat_0.1.0_aarch64.dmg /tmp/dmg-stage
hdiutil verify glchat_0.1.0_aarch64.dmg

Такой .dmg монтируется и содержит glchat.app и ссылку Applications, но без красивой раскладки иконок — для локальных проверок и CI этого достаточно, для публикации лучше собрать обычным make desktop-build.

Артефакты без .dmg (для автообновления) собираются где угодно: make desktop-build-app.

macOS: подпись Developer ID и нотаризация — сознательно не делаем

Решение владельца (2026-09-26): подпись Developer ID и нотаризацию не делаем — это платный аккаунт Apple Developer, а приложение распространяется внутри своего круга. Распространяется неподписанный .dmg; действия при первом запуске выполняет конечный пользователь (ниже). Подпись артефакта обновления (ed25519, ключ инстанса) от этого не зависит и проверяется всегда — её отсутствие или порча артефакта установку отменяют.

Что делает конечный пользователь (короткая инструкция, её же можно передать вместе с файлом):

  1. Открыть .dmg и перетащить glchat в Applications.
  2. Первый запуск — правый клик по приложению → «Открыть» → «Открыть» в диалоге «разработчик не может быть проверен». Дальше приложение запускается обычным двойным кликом.
  3. Если macOS не показывает кнопку «Открыть» (карантин): в терминале xattr -dr com.apple.quarantine /Applications/glchat.app, либо «Системные настройки → Конфиденциальность и безопасность → Всё равно открыть».

Текущий установщик релиза 0.1.1: desktop/src-tauri/target/release/bundle/dmg/glchat_0.1.1_aarch64.dmg (3,1 МБ, sha256 7f93609164dc5fdfe6255b88eaf909c02ba819a7e4fb79951cc50b142da825c0). Собирается make desktop-build (или bundle_dmg.sh --sandbox-safe в headless).

Если однажды понадобится подпись, команды остаются прежними (переменные читает сам Tauri, в репозиторий они не попадают):

security find-identity -v -p codesigning          # проверить наличие сертификата
export APPLE_SIGNING_IDENTITY="Developer ID Application: … (TEAMID)"
export APPLE_ID="…@example.com"
export APPLE_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="TEAMID"
make desktop-build                                 # Tauri подпишет и отправит на нотаризацию
xcrun stapler validate desktop/src-tauri/target/release/bundle/macos/glchat.app
spctl --assess --type execute --verbose=4 desktop/src-tauri/target/release/bundle/macos/glchat.app

Проверка «не подписано»: codesign -dv --verbose=4 <app> покажет Signature=adhoc.

Windows (x64): MSI/NSIS + подпись

Сборка идёт на Windows-машине или в CI (windows-latest), Rust + Node 22:

npx @tauri-apps/cli@^2 build --bundles nsis,msi
# подпись (EV/Trusted Signing или signtool с сертификатом):
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
  /f cert.pfx /p $env:CERT_PASSWORD `
  desktop\src-tauri\target\release\bundle\nsis\glchat_0.1.0_x64-setup.exe

Артефакты обновления (*.nsis.zip/*.msi + .sig) публикуются тем же скриптом: bash scripts/desktop-release.sh --artifact <файл> --target windows --arch x86_64.

Linux (AppImage + deb)

Сборка на Ubuntu 24.04/26.04 (нужны libwebkit2gtk-4.1-dev, librsvg2-dev, patchelf, libssl-dev, build-essential):

sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf libssl-dev build-essential
npx @tauri-apps/cli@^2 build --bundles appimage,deb
bash scripts/desktop-release.sh --artifact desktop/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz \
  --target linux --arch x86_64

На Wayland нативная тряска окна при несохранённых изменениях не работает (композитор запрещает set_position) — остаётся CSS-тряска контента; это ожидаемое поведение, а не дефект.

Ключ подписи обновлений

Приватный ключ — только в CI-секретах (TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PASSWORD); локально — build/desktop-keys/ (каталог в .gitignore). Публичный ключ уже вшит в сборку и меняется только вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.