Skip to content

Onboarding GUI: отдельный импорт существующего сервера, необязательный email, SSH-ключ в keychain - #26

Merged
Ap3x0s merged 35 commits into
mainfrom
dev
Sep 24, 2026
Merged

Ap3x0s merged 35 commits into
mainfrom
dev

Conversation

@Ap3x0s

@Ap3x0s Ap3x0s commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Что внутри

Onboarding GUI: разделение двух сценариев добавления сервера, необязательный email, переиспользование SSH-доступа и доводка карточки сервера. Дизайн и план зафиксированы в docs/superpowers/.

Ветка dev синхронизирована с актуальной main (dbedd4c).

1. Необязательный email при развёртывании

  • GUI: два взаимоисключающих режима — Указать email (по умолчанию) и Продолжить без email с предупреждением. emailMode обязателен на уровне контракта.
  • Bash: xrayebator quickstart --without-email — тот же broad-путь, но Certbot получает --register-unsafely-without-email; фиктивный адрес не подставляется, интерактивные меню-промпты не тронуты.
  • Цена режима задокументирована: без email нет уведомлений о продлении и email-восстановления ACME-аккаунта.

2. «Развернуть новый сервер» vs «Подключить существующий»

  • Пустой Dashboard — две карточки; при непустом списке Добавить открывает тот же выбор.
  • Импорт предлагает пароль или ключ — тот же выбор, что и при развёртывании.
  • Одна read-only команда xrayebator inspect --json (менеджер, Xray, профили, маркеры подписки, geo). Никаких quickstart/happ-setup/установки/обновления/миграций/restart/firewall — инварианты сторожит статический тест.
  • Частичная установка импортируется с честными статусами; local-only/недоступная подписка не отображается как рабочая.
  • Повторный импорт того же host+port — upsert без дубликата; id карточки и host-key pin сохраняются.
  • Мастер импорта показывает индекс шагов и живую консоль выполняемой работы (SSH, inspect, статусы компонентов, проверка подписки, итог).

3. SSH-доступ в системном keychain

  • Выбранный через native dialog ключ сохраняется через keytar (Windows Credential Manager / macOS Keychain / Secret Service) и переиспользуется после перезапуска.
  • SSH-пароль сохраняется в keychain после первого успешного входа и переиспользуется; в карточке — только несекретный passwordCredentialId.
  • Renderer получает только credential id и отображаемые имена; байты ключей и значения паролей не пересекают preload и не попадают в electron-store, логи или командные строки.
  • Отдельный sudo-пароль и passphrase ключа не сохраняются — запрашиваются при необходимости.
  • При недоступном keychain plaintext-фолбека нет: ключ живёт в памяти main process до выхода (before-quit wipe), UI предупреждает; неудачная запись нового пароля очищает устаревшую ссылку.
  • Удаление последней карточки с credential чистит запись keychain; общие ссылки сохраняются. Host-key TOFU pinning усилен не был и не ослаблен.

4. Карточка сервера и Server Settings

  • Карточка: заголовок с локацией, статус установки цветным текстом (зелёный «Настроен» — тот же --success, что у точки онлайн), ряд плиток с иконками — ОС, активные маршруты, SSH-доступ (user@host:port + где лежит секрет). Смена доступа — в меню из трёх точек.
  • Значения плиток берутся из карточки и read-only диагностики, ничего не зашито под Ubuntu/один маршрут/root.
  • Server Settings: автоподключение по сохранённому credential и сразу профильная панель. Форма доступа — только когда секрета нет или подключение не удалось; «Изменить доступ» открывает её с editingAccess=true.
  • Из Server Settings убраны статус-бокс доступа и таблица «Состояние установки» как дубли консолей развёртывания/импорта.

5. Исправления, найденные при тестировании

  • apt-lock гонка: quickstart падал с apt-get install nginx failed на свежем Ubuntu VPS — unattended-upgrades вызывает dpkg по каждому пакету и держит lock ~10 минут, а голый flock-опрос проскакивает в зазор. Теперь каждая установка ждёт активного воркера unattended-upgrade (бюджет 12 минут) и получает -o DPkg::Lock::Timeout=180; регрессия закреплена validation/test-apt-lock-race.sh.
  • SIGPIPE в CI: validation-скрипты падали с tr: write error: Broken pipe на Ubuntu (pipefail + рано выходящий читатель).
  • Токен подписки в логах: URL подписки — bearer credential, маскируется (…) в консоли развёртывания и в консоли импорта.

Изменённые слои

Слой Что
xrayebator quickstart --without-email, inspect --json, apt-lock бюджет и DPkg::Lock::Timeout, dispatch/help
src/main/core ssh-keychain.ts (keytar + password store), ssh-access.ts (resolver), server-inspector.ts (+ maskSubscriptionUrl, onLog), async onAuthenticated в ssh-client, email-mode в deployer, password-поля store, идемпотентный upsert
src/main/ipc-handlers.ts native selection → keychain, servers:import с шагами и логами, сохранение/очистка парольного credential
src/renderer Dashboard (карточки, плитки, меню доступа), ImportServer (консоль), ServerSettings (автоподключение), SshAccessForm, переводы RU/EN/ZH
тесты 14 unit-файлов (5 новых: keychain, store, inspector, deployer, ui-contracts), type-contracts в tsconfig.contracts.json (часть npm run typecheck), 2 новых validation-скрипта
docs desktop-gui/security/configuration/architecture/testing/troubleshooting ×3 языка, README ×3, CLAUDE.md, CHANGELOG (Unreleased)

Проверки

  • CI на актуальном head: bash-validation success (26 validation-скриптов, Ubuntu), test success, сборки Windows + macOS Intel/Apple Silicon — success.
  • npm run typecheck (node + web + contracts) — PASS; npm run build — PASS.
  • npm test — 83/84: единственная ошибка shell-command.test.ts (/bin/sh отсутствует на Windows, идентично baseline; Linux — источник истины).
  • Локальный прогон Bash-валидаций на Windows: часть падений объясняется отсутствием jq/uuidgen в Git Bash; Linux CI — источник истины.
  • Проверено на живом тестовом VPS: inspect --json возвращает корректный JSON, импорт проходит, карточка заполняется реальными ОС/маршрутами/пользователем.

Замечания к merge

  • Поведение с email по умолчанию не изменилось — старый путь покрыт тем же validation-скриптом.
  • inspect --json выводит новые поля (subscription_service, опц.), парсер GUI устойчив к лишним полям.
  • Требование к серверу: импорт существующего сервера вызывает xrayebator inspect --json, поэтому на сервере нужна версия менеджера с этой командой (появилась в этой ветке; в main её ещё нет). На сервере со старым менеджером импорт вернёт ошибку — лечится xrayebator update dev или обновлением из терминала. GUI сейчас показывает в этом случае общий текст про некорректный JSON; отдельное сообщение «на сервере старая версия, обновите менеджер» — возможная отдельная задача.
  • Легаси privateKeyPath остаётся одобренным dialog'ом fallback для старых карточек; удалить можно отдельной задачей, когда ни одна карточка на него не ссылается.

tr | awk/sed с ранним exit обрывал pipe на Ubuntu: set -euo pipefail
превращал 'tr: write error: Broken pipe' в FAIL (CI run 35892796234),
хотя в Git Bash тот же код проходил. Читаем tr-вывод целиком в переменную
и режем блоки из here-string, где досрочный выход приёмника безвреден.
После email/inspect валидации и onboarding unit-тестов count'ы устарели:
24 -> 25 validation-скриптов, 9 -> 14 unit-файлов, плюс строки таблиц и
пояснение, что npm run typecheck проверяет и tests/type-contracts
(Vitest-транспиляция регрессии типов не ловит). Убраны и устаревшие
'39 тестов' / 'checkout on main'.
Первоначальное ограничение 'только ключ' пользователь отверг на ручном
тесте: мастер импорта должен давать тот же выбор пароль/ключ, что и
страница развёртывания. Убран main-process guard по authMethod, форма
использует общий isSshAccessReady, подсказка про ключ показывается только
в режиме ключа. Read-only инвариант импорта (inspect --json, без repair)
не затронут; спецификация и переводы обновлены.
Пустой Dashboard: две карточки сценариев шли в две колонки; по просьбе
пользователя выстроены вертикально (сверху 'Развернуть новый сервер',
снизу 'Подключить существующий'). Иконки и их боксы увеличены
(44->54px бокс, глиф 22->26px), избыточный mobile-override убран —
сетка теперь всегда одноколоночная.
…-upgrades

Корень (зафиксировано на живом VPS 31.76.240.21, Ubuntu 22.04, 2026-09-23):
на свежеподнятой машине unattended-upgrades прогоняет полный security-апгрейд
(~90 пакетов, 21:10:39 -> 21:20:23). dpkg вызывается ОТДЕЛЬНО на каждый пакет,
между пакетами flock свободен 4-6 секунд. _apt_wait_lock на голом flock
проскакивал в такой зазор, apt-get install nginx стартовал и умирал по своему
lock-timeout (в Ubuntu дефолт DPkg::Lock::Timeout=120) или по внешнему kill —
quickstart падал с «apt-get install nginx failed» при формально свободном lock.
Фикс 11.08 (ожидание lock) помогал только против постоянно занятого lock.

Что сделано:
- _apt_wait_lock дополнительно считает занятым время активного рабочего
  процесса /usr/bin/unattended-upgrade (pgrep -f по полному пути: comm ядро
  обрезает до 15 символов, и воркер, и резидентный shutdown-helper выглядят
  одинаково «unattended-upgr» — по comm не различить, проверено экспериментом);
- бюджет ожидания в quickstart 300 -> 720 секунд (покрывает наблюдённый прогон
  ~10 мин), текст ошибки — с явным предложением повторить позже;
- явный -o DPkg::Lock::Timeout=180 (APT_LOCK_OPTS) во все apt-get install,
  внешний timeout install-вызовов >= 300, чтобы kill не обрывал ожидание apt;
- validation/test-apt-lock-race.sh — статический контракт по всем четырём
  пунктам.

На сервере после прогона очереди: lock свободен, dpkg -C чист, dry-run install
nginx rc=0 — повторный quickstart проходит без ручных вмешательств.
Не влияет: domain-режим подписки и self-steal получили те же APT_LOCK_OPTS,
их логика не менялась.
Ручной тест показал: при входе в Server Settings пароль спрашивался заново
(в keychain лежал только ключ, не пароль), а панель профилей открывалась под
незакрытой формой доступа. Теперь SSH-пароль после ПЕРВОГО успешного входа
кладётся в OS keychain (отдельный service ssh-password) и переиспользуется;
в карточке — только несекретный passwordCredentialId. Отдельный sudo-пароль и
passphrase приватного ключа не сохраняются.

- ssh-keychain: createSshPasswordStore (save/load/remove, guard размера/CR-LF,
  namespace ssh-password, без plaintext-фолбека при недоступном keychain);
- ssh-client: onAuthenticated стал async и дожидаемся промиса до resolve connect,
  чтобы keychain-запись завершилась до того, как UI посчитает вход успешным;
- ssh-access: resolveStoredSshPassword (введённый вручную пароль приоритетнее
  сохранённого; загрузка из keychain ставит passwordPersisted=true);
- ipc-handlers: persistSshPassword на onAuthenticated (сущ. сервер) и после
  deploy/import; ручой ввод сбрасывает флаг через setTypedSshPassword, чтобы
  новый пароль перезаписывал запись; servers:remove чистит keychain по
  countPasswordCredentialReferences;
- ServerSettings: auto-connect по shouldAutoConnectServer и форма доступа
  сворачивается в статус «доступ подтверждён» после успеха (кнопка Изменить доступ);
- plan + i18n + тесты (vault/resolver/store clear/auto-connect/typed re-persist/async await).

TDD: vault, resolver, store-clear и typed-reset тесты падали до реализации. typecheck
(node+web+contracts) и build зелёные; unit 80/81 (sole /bin/sh caveat).
Старые формулировки («пароли никогда не сохраняются») противоречили новой
реализации. README/GUI/security на трёх языках описывают: SSH-пароль сохраняется
в OS keychain после первого успешного входа и переиспользуется; отдельный
sudo-пароль и passphrase не сохраняются; Server Settings подключается
автоматически, а форма доступа сворачивается в статус.
Добавлена секция quickstart «apt-get install nginx failed» с корнем (per-package
dpkg у unattended-upgrades и зазор flock между пакетами) и контрактами (worker
check, 12-мин бюджет, DPkg::Lock::Timeout=180). Счётчик validation-скриптов
25->26, новая строка test-apt-lock-race.sh в трёх testing-доках и CLAUDE.md.
Импорт показывал только справа индекс фаз, без консоли — в отличие от
развёртывания, где видно, какие команды реально уходят на сервер. Теперь
ServerInspector пишет строки хода работ (SSH connect, \$ xrayebator
inspect --json, код, статусы компонентов, probe подписки, итог) через onLog,
IPC шлёт их как { log } на тот же канал servers:importEvent, а ImportServer
рендерит консоль под индексом фаз по образцу DeployServer.

Событие импорта стало union { step } | { log }; preload форвардит как есть.
URL подписки — bearer credential, поэтому перед выводом в консоль токен
маскируется maskSubscriptionUrl (TDD: тест падал до реализации).
«Доступ к серверу подтверждён» не объяснял, что это значит. Статус-карточка
Server Settings теперь показывает фактический вход user@host:port и строку о
том, где лежит секрет и что будет при следующем открытии: пароль/ключ
сохранены в системном keychain (авто-вход), ключ только на этот сеанс,
секрет не сохранён. Предупреждающий тон — для не-персистентных случаев.

Ключи во всех трёх локалях.
Триггеры «Добавить» и переключателя языка несли «+» перед подписью; текст
сидел чуть ниже центра бокса. Убрал «+». Корень смещения — базовая линия
inline-SVG шеврона при наследуемой HeroUI высоте: задал обоим триггерам
(.langSelect — общий класс) фиксированную высоту 34px с box-sizing и
симметричным паддингом, value-спан стал flex, иконка display:block.
Проверено в собранном CSS: .langSelect идёт после .button и побеждает каскад.
В меню профилей статус-бокс выглядел излишне: пользователь уже внутри сервера,
а кнопка смены доступа там не нужна. Сводка (user@host:port + где лежит секрет:
пароль/ключ в системном хранилище либо только на сеанс) и действие «Изменить
доступ» переехали в правую часть карточки сервера в дашборде.

- accessSummary() в server-access.ts (чистая функция, TDD: тест на endpoint и
  4 состояния секрета — passwordSaved/keySaved/sessionOnly/notSaved);
- Dashboard: правая колонка cardAccess с endpoint, подписью секрета и ghost-
  кнопкой «Изменить доступ»; предупреждающий тон для не-персистентных случаев;
- App: view settings получил editingAccess, onEditAccess открывает настройки
  сразу с формой доступа;
- ServerSettings: проп editingAccess отключает автоподключение, чтобы форма
  показалась для смены доступа; удалён осиротевший reset();
- i18n: новые dashboard.secret*/changeAccess в трёх локалях.
Состояние установки и так показывается в консоли развёртывания/импорта, а в
карточке остаётся компактный чип setup status. Дублирующая 4-строчная таблица
только загроможждала страницу профилей. Данные diagnostics по-прежнему
сохраняются при импорте и питают чип, удалён лишь их вывод в GUI.

Вместе с ней удалены осиротевшие CSS-блоки diagnosticsCard/diagnosticsGrid и
неиспользуемые ключи settings.diagnostics.* + статус-ключи доступа (заменены
на dashboard.secret*).
GUI-доки (EN/RU/ZH), спецификация и план описывают новую раскладку: сводка
доступа и «Изменить доступ» — на карточке сервера, форма доступа в настройках
только при отсутствии секрета или ошибке, таблицы «Состояние установки» в
профилях больше нет. Убрано устаревшее «+ Add».
Сводка доступа выглядела как обрывистый текст поверх карточки, а «Изменить
доступ» — как синяя текстовая ссылка, выбиваясь из ряда «Ключи/Настройки».
Теперь это отдельная панель (фон bg-1, рамка, radius-md, заголовок «Доступ»
капителью) и обычная secondary-кнопка с иконкой карандаша, как соседние
кнопки карточки.

Подписи про секрет сокращены до одной строки (пароль/ключ сохранён в системе,
только на этот сеанс, секрет не сохранён) — раньше они переносились на две
строки и распирали панель. Правка в трёх локалях.

Проверено предпросмотром собранного CSS: панель центрирует endpoint, подпись
и кнопку по правому краю, обе карточки не разъезжаются.
Раскладка карточки приведена к виду, который пользователь показал как эталон:
заголовок с локацией под ним, ряд из трёх плиток с иконками (ОС, активные
маршруты, SSH-доступ) и ряд кнопок внизу. Действие «Изменить доступ» убрано с
вида — оно живёт в меню из трёх точек в плитке доступа, как на образце.

- плитки ОС/маршруты/доступ: иконка + значение + подпись, все данные уже
  приходят из бэкенда (os и profile_count из inspect, routesCount и username из
  карточки), ничего не зашито под Ubuntu/один маршрут/root;
- новые ключи dashboard.statOs/statRoutes в трёх локалях (иначе i18next рисовал
  сырые ключи — это было видно на скриншоте пользователя);
- статус установки: плашка-чип заменена на обычный текст зелёным (--success,
  тот же цвет, что у точки онлайн), «Настроен частично» — жёлтым;
- удалены осиротевшие стили прежней панели доступа и неиспользуемый импорт Chip.
GUI-доки и спецификация отражают фактический вид: заголовок с локацией, статус
цветным текстом без плашки, три плитки (ОС, маршруты, SSH-доступ) и меню из трёх
точек для смены доступа. Отдельно зафиксировано, что значения плиток приходят из
карточки и read-only диагностики, а не зашиты под Ubuntu/один маршрут/root.
По замечаниям к скриншоту:
- точка активности поднята на уровень середины флага, а не по центру всего
  блока; для этого шапка стала сеткой из жёлоба и контента — точка и пин стоят
  в жёлобе, название и локация в своей колонке;
- пин места переехал в тот же жёлоб ниже, подпись локации начинается под флагом;
- значки плиток увеличены с 18 до 27 px;
- плитка маршрутов стала однострочной (число и подпись в ряд), но прижата влево,
  как содержимое соседних плиток, а не по центру.

Причина, по которой прошлые правки шапки не применялись: ниже по файлу
оставались дубликаты .cardTitle/.flag, перекрывавшие новые правила. Дубликаты
удалены, проверено скриптом на повторяющиеся селекторы.
В мастере импорта токен уже маскировался (maskSubscriptionUrl), а лог
развёртывания печатал URL подписки целиком. URL подписки — bearer credential,
одного полного URL достаточно для скачивания маршрутов, поэтому обе консоли
теперь показывают только начало и конец токена.

Тест deployer проверяет, что полный токен в вывод не попадает.
Мастер импорта получил живую консоль с командами и статусами, но в GUI-доках
она не была описана. Добавлено описание консоли в трёх языках, а в security-доках
зафиксировано, что токен подписки маскируется в обеих консолях, пароли и байты
ключей туда не пишутся.
Изменения после v0.5.0 (email-режим, импорт, keychain-пароль, консоль импорта,
маскирование токена, apt-lock фикс) не были отражены в CHANGELOG.
@Ap3x0s
Ap3x0s merged commit 79358f5 into main Sep 24, 2026
6 checks passed
Ap3x0s added a commit that referenced this pull request Sep 24, 2026
Версия 0.5.5, релизные заметки RU/EN и запись в CHANGELOG.

Содержание релиза — то, что уже влито в main через PR #26 и проверено на
живом VPS: импорт существующих серверов (read-only inspect), сохранение
SSH-пароля в системном keychain, необязательный email при развёртывании,
живая консоль импорта, маскирование токена подписки и фикс гонки apt-lock.

Заметки написаны в стиле v0.5.0 — разница объясняется на языке пользователя:
было «только развернуть новый сервер», стало «развернуть или подключить
существующий».
Ap3x0s added a commit that referenced this pull request Sep 24, 2026
Версия 0.5.5, релизные заметки RU/EN и запись в CHANGELOG.

Содержание релиза — то, что влито PR #26 и проверено на живом VPS: импорт
существующих серверов (read-only inspect), сохранение SSH-пароля в системном
keychain, необязательный email при развёртывании, живая консоль импорта,
маскирование токена подписки и фикс гонки apt-lock.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant