diff --git a/desktop/README.md b/desktop/README.md index bfcf0c6..733d108 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -20,12 +20,18 @@ desktop/ ```bash 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-окружения он +падает (см. «Что не сделано»). + Сборка релиза с артефактами автообновления использует ключ подписи: ```bash @@ -74,17 +80,205 @@ make desktop-build Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и доставляется после `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 # манифест последнего релиза — его и отдаёт инстанс +``` + +Публикация релиза: + +```bash +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.-.signature/url`) +и кладёт его и как `.json`, и как `latest.json`. Несколько платформ +одной версии (macOS + Windows + Linux) складываются в один манифест: запускайте +скрипт по разу на артефакт. + +`latest.json` нужен потому, что обёртка приходит со **своей установленной** +версией: «новее ли релиз» решает клиент сравнением semver +(`tauri-plugin-updater`), поэтому на запрос любой версии сервер отдаёт +последний манифест, а если есть манифест конкретной версии — его. Без этого +обновление не получил бы никто, кроме тех, у кого уже стоит последняя версия. + +Владельцу прокси добавлять ничего не нужно: `/updates/...` уходит в +catch-all `handle` и доходит до приложения (так же, как статика клиента). +Вариант «раздавать обновления статикой мимо приложения» требует правила +`rewrite` на `latest.json` с подстановкой `target`/`arch` в Caddy — лишняя +работа без выгоды, поэтому не рекомендуется. + +Проверка (стенд): + +```bash +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//channel/`, для личной беседы `glchat://dm/`). + +Ограничение (честно): ОС не сообщает, что активация вызвана именно кликом по +уведомлению, поэтому переход сработает и при обычном возврате в приложение в +течение этих 10 секунд. Клик по уведомлению старше 10 секунд только поднимает +окно. Полноценный колбэк требует своего моста к `UNUserNotificationCenter` +(`UNUserNotificationCenterDelegate`) — отдельная задача. + +## Ресурсы и холодный старт + +Замеры собраны скриптом `scripts/desktop-perf.py` (macOS arm64, Mac на M-серии, +инстанс `gl.mhspx.su` через внешний прокси; подробности — `docs/PERF.md`): + +| Метрика | Значение | Бюджет (docs/client-tauri.md §1) | +|---|---|---| +| Холодный старт (`open` → процесс → строка «загружена страница») | 708–740 мс (три прогона) | ≤ 2 с | +| Из них: запуск процесса | 45–74 мс | — | +| Из них: загрузка страницы клиента | 637–682 мс | — | +| RAM, клиент открыт и простаивает (RSS: обёртка + 4 процесса WebKit) | 252 МБ (117 + 135) | ≤ 350 МБ | +| RAM, окно свёрнуто в трей (`--minimized`) | 251 МБ | ≤ 350 МБ | +| Физический след (Activity Monitor) | 103 МБ | — | + +Наблюдения: + +- свёрнутое в трей окно памяти не освобождает: webview с загруженным клиентом + продолжает жить (так работают WKWebView и трей-режим) — экономия была бы + только при выгрузке страницы, а это потеря мгновенного возврата в клиент; +- первый запуск сразу после сборки бандла (холодный кэш файловой системы и + страницы) дал 1,26 с — тоже в бюджете; +- почти вся память — WebKit (страница клиента), сама обёртка занимает ~117 МБ. + +Повторить замер: + +```bash +make desktop-build-app # свежий .app (+ артефакты обновления) +python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта + простой в трее +``` + ## Что не сделано (открытые пункты Фазы 6) -- **Манифест обновлений на сервере**: `GET /updates/{target}/{arch}/{current_version}` - ещё не публикуется — нужна статика на стороне инстанса (Caddy) и релизный - контур с приватным ключом в CI. Клиентская часть готова: подпись проверяется, - downgrade отклоняется, при недоступном манифесте ошибка только логируется. -- **Клик по уведомлению** → открытие канала: у `tauri-plugin-notification` нет - колбэка действия на desktop, нужен свой мост к `UNUserNotificationCenter`. +- **Автообновление end-to-end не проверено на реальном релизе**: манифест + отдаётся и разбирается (проверено на стенде), но установка «поверх старой + версии» требует публикации версии с бо́льшим номером — это действие оператора + (см. «Публикация релиза» выше). - **Аватары в уведомлениях** (thumb/буква) и группировка — сейчас иконка приложения. - **Захват экрана и микрофон**: разрешения macOS выдаёт webview, отдельных экранов-инструкций у обёртки нет. -- **Подпись и нотаризация сборки** macOS/Windows: Developer ID/EV только в CI. +- **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: + +```bash +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): + +```bash +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 ` покажет `Signature=adhoc`. + +### Windows (x64): MSI/NSIS + подпись + +Сборка идёт на Windows-машине или в CI (`windows-latest`), Rust + Node 22: + +```powershell +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`): + +```bash +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`). Публичный ключ уже вшит в сборку и меняется только +вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.