Files
glchat/desktop
grendervill 19d2501ab0 docs(desktop): замеры ресурсов на финальной сборке
Числа в README приведены к сборке, которая поставляется: холодный старт
773–898 мс (процесс 74–78 мс + страница 696–821 мс), RSS 254 МБ
(обёртка 117 + WebKit 137), в трее 256 МБ, физический след 106 МБ.
Прошлый прогон (708–740 мс, 252 МБ) снят с бинарника до правки выхода из трея —
расхождение в пределах разброса, но в документации должны стоять числа
отгружаемой сборки.
2026-09-22 23:21:52 +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-окружения он падает (см. «Что не сделано»).

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

# ключ и пароль не коммитятся: приватный ключ живёт в 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 включить или выключить автозапуск при установке скриптом

Настройки

Хранятся в 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).

Манифест отдаёт сам инстанс (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.0.json                # манифест версии
    └── latest.json               # манифест последнего релиза — его и отдаёт инстанс

Публикация релиза:

make desktop-build                              # собрать бандл (.app/.dmg) и артефакты обновления
sudo make desktop-release                       # разложить в /opt/glchat/updates + создать манифест
make desktop-release-upload HOST=user@server    # то же, но по SSH (sudo спросит пароль в терминале)
make desktop-release ARGS='--dry-run'           # посмотреть план, ничего не записывая

Скрипт 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) складываются в один манифест: запускайте скрипт по разу на артефакт.

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 .
# в логе обёртки (~/Library/Logs/su.mhspx.glchat/glchat.log) —
# «обновлений нет» (манифест разобран) вместо
# «проверка обновлений не удалась: … 404»

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

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

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

Клик по уведомлению: у 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)

  • Автообновление end-to-end не проверено на реальном релизе: манифест отдаётся и разбирается (проверено на стенде), но установка «поверх старой версии» требует публикации версии с бо́льшим номером — это действие оператора (см. «Публикация релиза» выше).
  • Аватары в уведомлениях (thumb/буква) и группировка — сейчас иконка приложения.
  • Захват экрана и микрофон: разрешения macOS выдаёт webview, отдельных экранов-инструкций у обёртки нет.
  • Developer ID и нотаризация macOS, EV/Trusted Signing для Windows — только в CI с платными сертификатами (ниже — точные команды).
  • .dmg: собирается только из обычного терминала (Finder/AppleScript).
  • Windows/Linux-бандлы: конфигурация кросс-платформенная, но собирались и проверялись только macOS-arm64.

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

macOS: .dmg

bundle_dmg.sh монтирует образ (hdiutil) и управляет Finder через AppleScript, поэтому из песочницы агента и из headless-сессии он падает. Из обычного терминала на macOS:

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

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

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

Нужны сертификат Developer ID Application в связке ключей и пароль приложения (appleid.apple.com → App-Specific Passwords):

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

Переменные читает сам Tauri (APPLE_*), в репозиторий они не попадают. Проверка «не подписано»: 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). Публичный ключ уже вшит в сборку и меняется только вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.