Open
Conversation
Версия 0.5.5, релизные заметки RU/EN и запись в CHANGELOG. Содержание релиза — то, что уже влито в main через PR #26 и проверено на живом VPS: импорт существующих серверов (read-only inspect), сохранение SSH-пароля в системном keychain, необязательный email при развёртывании, живая консоль импорта, маскирование токена подписки и фикс гонки apt-lock. Заметки написаны в стиле v0.5.0 — разница объясняется на языке пользователя: было «только развернуть новый сервер», стало «развернуть или подключить существующий».
Релиз v0.5.5 упал на шаге npm test: на ubuntu-раннере нет libsecret-1.so.0, поэтому keytar не загружается и весь tests/unit/ssh-keychain.test.ts падает как suite (75 тестов из 75 проходят, но файл не стартует). В этом же файле libsecret уже ставился для job build, а для test — нет. Тесты проходили локально на Windows (Credential Manager) и в ci-linux (там GUI-тесты не запускаются вообще), поэтому проблема всплыла только на первом релизном прогоне после добавления ssh-keychain.test.ts.
… владельца AP3X0 Поле author у GitHub-релиза неизменяемо: выпущенный ботом релиз нельзя «перекрестить» на человека, поэтому имя публикуемого аккаунта надо выбрать до публикации. Workflow теперь: - preflight job проверяет наличие docs/releases/<tag>.en.md и способность секрета AP3X0 создавать релизы (пробным черновиком, который сразу удаляется) — до того, как потрачены ~15 минут на три платформы; - job release публикует от AP3X0, если секрет задан и прошёл пробник, иначе остаётся на встроенном GITHUB_TOKEN. Без секретов поведение не меняется: автор github-actions[bot].
Изменение workflow меняет наблюдаемое поведение (быстрые падение на плохом токене, имя автора релиза), поэтому документация по CI обновляется в трёх локалях сразу: ru/en/zh.
Предыдущая версия падала на preflight, если токен AP3X0 не может создавать релизы, — но релиз можно опубликовать и встроенным GITHUB_TOKEN, ломать из-за этого выкатку неверно. Теперь обе проверки (текст релиза, боеспособность токена) только предупреждают, а job release берёт AP3X0 лишь когда preflight подтвердил его права (output as_user); иначе — github-actions[bot]. Так исключён и худший сценарий: падение публикации после трёх платформ.
profile-revoke --name N [--full]: перевыпуск sub_token (обёртка логики TUI-пункта 2, handler читает токен на каждый запрос — рестарт subhttp не нужен); --full меняет и uuid во всех inbound'ах портов профиля — уже скачанные конфиги отваливаются сразу (утёкшая в чат ссылка перестаёт быть вечным доступом; токен-революция этого не лечит). uuid в profile JSON совпадает с конфигом всегда: запись профиля идёт после валидации конфига, сбой записи — откат config.json из бэкапа транзакции. Сроки: profile-create --expire (epoch/мс/'2006-12-31[ 23:59]' → нормализация в сек), profile-expire --expire DATE|none (немедленное принуждение), expire-check (пакетно, идемпотентно: без изменений — без рестарта, иначе таймер ронял бы Xray каждые 10 мин). Принуждение серверное: systemd-таймер xrayebator-expire (10 мин) ставится миграцией expire_timer_2026 (main_menu+quickstart, parity-guard); disable снимает клиента в .expire_clients+fлаг (inbound'ы живут: пустой inbound при продлении восстановим без new shortId — _remove_profile_uuid выкидывал бы inbound целиком), enable возвращает со снимка (с актуальным uuid на случай revoke между циклами) или rebuild-ом из routes (flow: vision для tcp/utls/xudp, '' для mux/grpc/xhttp). profiles-вывод обогатился expire/expire_disabled. --expire в create не позволяет срок в прошлом. bypass groups — JSON-описание 7 bundle-групп для GUI-чекбоксов; bypass-бандл уже был. uninstall.sh снимает expire-таймер (прецедент ip-renew: иначе долбит удалённый бинарь). JSON-only контракт: backup_config и все helpers — после exec 3>&1 switch (нашёл тест: '-> Бэкап конфига' перед JSON ломал парсинг). Тест: validation/test-profile-revoke-expire-cli.sh (27/27 всей валидации зелёные; два jq-баг-фикса по дороге: index(.port) внутри map по массиву ports, снимок-выражение с -биндингом).
groups раньше возвращал только счётчик доменов — GUI-чекбоксам нужен состав группы, чтобы считать покрытие («включена/частично/выключена») без второго запроса. Добавлен unbundle: снятие группы одной мутацией и одним рестартом Xray (штучный remove --domain = N рестартов на группу из ~60 доменов, для интерактивного чекбокса неприемлемо). Оба — JSON-only: backup_config печатает статус только после exec 3>&1 switch.
Revoke: кнопка у профиля открывает центральную плашку на блюренной подложке
(AlertDialog.Backdrop + .blurBackdrop; существующие диалоги не тронуты) с двумя
режимами — «только новая ссылка» (токен, старые конфиги живут) и «полный отзыв»
(uuid во всех маршрутах, устройства отваливаются, второй шаг-подтверждение);
после успеха показывает новую ссылку и перечитывает список профилей.
Срок: чип «до ДД.ММ.ГГГГ»/«срок истёк» в карточке, диалог с пресетами +7/30/90/365
и «снять срок», поле срока при создании профиля. Вся арифметика вынесена в
src/shared/expire.ts (describeExpire/isFutureDate/presetDate) — покрыта юнит-тестами,
включая приоритет серверного флага expire_disabled над локальными часами.
Bypass: отдельная секция (BypassSection) — ленивая загрузка списка, свои домены
add/remove, reset с подтверждением, чекбоксы групп с состояниями
вкл/частично/выкл через bundle/unbundle.
Слои: BypassManager (SSH-обёртка над bypass CLI), IPC-каналы profiles:revoke/
setExpire + bypass:*, preload, типы, i18n ru/en/zh. typecheck чистый, vitest 89/90
(единственный красный — предсуществующий shell-command: spawnSync('/bin/sh')
недоступен под Windows-нодой, падает и на чистом дереве).
desktop-gui (ru/en/zh): из списка «GUI не предоставляет» убраны bypass и revoke (теперь есть), добавлено описание трёх новых возможностей Server settings — двухрежимный revoke, срок действия с серверным принуждением, bypass-группы; обновлён список CLI-команд, на которые отображается профильный API. configuration (ru/en/zh): в справочник добавлены profile-revoke, profile-expire, expire-check (таймер), bypass groups/unbundle и --expire у profile-create. Заодно поправлены два места, где доки отправляли за revoke «в терминал»: electron-store секция (ru/en) и ремарка про Server keys — перевыпуск токена теперь явно указан на карточках профилей.
profile-revoke --name N [--full]: перевыпуск sub_token (обёртка логики TUI-пункта 2, handler читает токен на каждый запрос — рестарт subhttp не нужен); --full меняет и uuid во всех inbound'ах портов профиля — уже скачанные конфиги отваливаются сразу (утёкшая в чат ссылка перестаёт быть вечным доступом; токен-революция этого не лечит). uuid в profile JSON совпадает с конфигом всегда: запись профиля идёт после валидации конфига, сбой записи — откат config.json из бэкапа транзакции. Сроки: profile-create --expire (epoch/мс/'2006-12-31[ 23:59]' → нормализация в сек), profile-expire --expire DATE|none (немедленное принуждение), expire-check (пакетно, идемпотентно: без изменений — без рестарта, иначе таймер ронял бы Xray каждые 10 мин). Принуждение серверное: systemd-таймер xrayebator-expire (10 мин) ставится миграцией expire_timer_2026 (main_menu+quickstart, parity-guard); disable снимает клиента в .expire_clients+fлаг (inbound'ы живут: пустой inbound при продлении восстановим без new shortId — _remove_profile_uuid выкидывал бы inbound целиком), enable возвращает со снимка (с актуальным uuid на случай revoke между циклами) или rebuild-ом из routes (flow: vision для tcp/utls/xudp, '' для mux/grpc/xhttp). profiles-вывод обогатился expire/expire_disabled. --expire в create не позволяет срок в прошлом. bypass groups — JSON-описание 7 bundle-групп для GUI-чекбоксов; bypass-бандл уже был. uninstall.sh снимает expire-таймер (прецедент ip-renew: иначе долбит удалённый бинарь). JSON-only контракт: backup_config и все helpers — после exec 3>&1 switch (нашёл тест: '-> Бэкап конфига' перед JSON ломал парсинг). Тест: validation/test-profile-revoke-expire-cli.sh (27/27 всей валидации зелёные; два jq-баг-фикса по дороге: index(.port) внутри map по массиву ports, снимок-выражение с -биндингом). (cherry picked from commit c0d4083)
groups раньше возвращал только счётчик доменов — GUI-чекбоксам нужен состав группы, чтобы считать покрытие («включена/частично/выключена») без второго запроса. Добавлен unbundle: снятие группы одной мутацией и одним рестартом Xray (штучный remove --domain = N рестартов на группу из ~60 доменов, для интерактивного чекбокса неприемлемо). Оба — JSON-only: backup_config печатает статус только после exec 3>&1 switch. (cherry picked from commit e5b8d3c)
Revoke: кнопка у профиля открывает центральную плашку на блюренной подложке
(AlertDialog.Backdrop + .blurBackdrop; существующие диалоги не тронуты) с двумя
режимами — «только новая ссылка» (токен, старые конфиги живут) и «полный отзыв»
(uuid во всех маршрутах, устройства отваливаются, второй шаг-подтверждение);
после успеха показывает новую ссылку и перечитывает список профилей.
Срок: чип «до ДД.ММ.ГГГГ»/«срок истёк» в карточке, диалог с пресетами +7/30/90/365
и «снять срок», поле срока при создании профиля. Вся арифметика вынесена в
src/shared/expire.ts (describeExpire/isFutureDate/presetDate) — покрыта юнит-тестами,
включая приоритет серверного флага expire_disabled над локальными часами.
Bypass: отдельная секция (BypassSection) — ленивая загрузка списка, свои домены
add/remove, reset с подтверждением, чекбоксы групп с состояниями
вкл/частично/выкл через bundle/unbundle.
Слои: BypassManager (SSH-обёртка над bypass CLI), IPC-каналы profiles:revoke/
setExpire + bypass:*, preload, типы, i18n ru/en/zh. typecheck чистый, vitest 89/90
(единственный красный — предсуществующий shell-command: spawnSync('/bin/sh')
недоступен под Windows-нодой, падает и на чистом дереве).
(cherry picked from commit 4d09047)
desktop-gui (ru/en/zh): из списка «GUI не предоставляет» убраны bypass и revoke (теперь есть), добавлено описание трёх новых возможностей Server settings — двухрежимный revoke, срок действия с серверным принуждением, bypass-группы; обновлён список CLI-команд, на которые отображается профильный API. configuration (ru/en/zh): в справочник добавлены profile-revoke, profile-expire, expire-check (таймер), bypass groups/unbundle и --expire у profile-create. Заодно поправлены два места, где доки отправляли за revoke «в терминал»: electron-store секция (ru/en) и ремарка про Server keys — перевыпуск токена теперь явно указан на карточках профилей. (cherry picked from commit df6cff9)
…GELOG/CLAUDE Второй проход по «карте возможностей»: revoke, сроки и bypass больше не числятся терминальными операциями в README (ru/en/zh), troubleshooting (ru/en/zh), configuration (ru/en), security (ru/en/zh) и architecture (ru/en/zh) — вместо «используйте терминал» указано, где это живёт в GUI, а про полный отзыв добавлено, что он меняет и клиентский ключ (уже скачанные конфиги перестают работать). CHANGELOG: секция Unreleased с тремя фичами. CLAUDE.md: в справочник CLI добавлены profile-revoke/profile-expire/expire-check, bypass groups/unbundle, --expire у profile-create, новые поля profiles-вывода, инварианты (порядок транзакции revoke, сохранение inbound'ов при expire) и обновлён счётчик validation-тестов (27). docs/testing: строка про новый test-profile-revoke-expire-cli.sh.
…GELOG/CLAUDE Второй проход по «карте возможностей»: revoke, сроки и bypass больше не числятся терминальными операциями в README (ru/en/zh), troubleshooting (ru/en/zh), configuration (ru/en), security (ru/en/zh) и architecture (ru/en/zh) — вместо «используйте терминал» указано, где это живёт в GUI, а про полный отзыв добавлено, что он меняет и клиентский ключ (уже скачанные конфиги перестают работать). CHANGELOG: секция Unreleased с тремя фичами. CLAUDE.md: в справочник CLI добавлены profile-revoke/profile-expire/expire-check, bypass groups/unbundle, --expire у profile-create, новые поля profiles-вывода, инварианты (порядок транзакции revoke, сохранение inbound'ов при expire) и обновлён счётчик validation-тестов (27). docs/testing: строка про новый test-profile-revoke-expire-cli.sh. (cherry picked from commit 2070c62)
Чип срока в карточке профиля раньше имел только два состояния (дата / истёк), а CSS-класс предупреждения остался неиспользованным. Теперь за 3 дня до срока чип подсвечивается жёлтым и объясняет в тултипе, что профиль отключится сам — успеть продлить. Ключ expireHintSoon добавлен в ru/en/zh (150/150/150). Проверено: typecheck чистый, 145 settings-ключей из кода резолвятся во всех локалях, 0 неиспользуемых CSS-классов, vitest 89/90 (единственный красный — предсуществующий shell-command: /bin/sh недоступен под Windows-нодой).
Чип срока в карточке профиля раньше имел только два состояния (дата / истёк), а CSS-класс предупреждения остался неиспользованным. Теперь за 3 дня до срока чип подсвечивается жёлтым и объясняет в тултипе, что профиль отключится сам — успеть продлить. Ключ expireHintSoon добавлен в ru/en/zh (150/150/150). Проверено: typecheck чистый, 145 settings-ключей из кода резолвятся во всех локалях, 0 неиспользуемых CSS-классов, vitest 89/90 (единственный красный — предсуществующий shell-command: /bin/sh недоступен под Windows-нодой). (cherry picked from commit b2348b8)
Корень всех трёх симптомов («bypass groups → код 1», «срок показывается бессрочно», «кнопка добавить падает») один: сервер работал на релизном коде, где новых команд нет вообще. GUI новее сервера, и вместо объяснения показывал пустое «код 1: » — менеджер печатает причину в stdout, а ошибка читала stderr. 1. Диагностика падений CLI (src/main/core/cli-failure.ts): describeFailure достаёт причину из stderr, а если пусто — из stdout («Неизвестная команда»), плюс isUnknownCommandFailure добавляет подсказку «обновите сервер». ProfileManager и BypassManager используют её вместо res.stderr. 2. Ловушка ветки: xrayebator update <branch> заменял скрипт, но НЕ писал .current_branch, а GUI-кнопка «Обновить Xrayebator» читает именно этот файл и по умолчанию берёт main. Итог: обновив сервер на dev, пользователь получал откат на релиз при следующем обновлении из GUI. Теперь ветка закрепляется после успешной замены скрипта, имя ветки валидируется до подстановки в URL. 3. profiles отдаёт expire_supported: true. Старый сервер поля не отдаёт, и GUI молча показывал «бессрочно» вместо «управление сроками недоступно». Теперь при отсутствии флага выводится честное предупреждение. 4. UX: «Добавить» больше не бьёт в сервер при пустом поле (пользователь просто ещё не написал домен, а получал ошибку). Регрессия: validation/test-update-branch-pinning.sh (28/28 в наборе).
Календарь: фиксированная высота шапки (месяц + стрелки) и строки дней недели — раньше названия месяцев разной длины и «Пн/Втр/Ср» дёргали стрелки по вертикали, и пользователь каждый раз искал курсором новое положение. Сетка теперь всегда 42 ячейки (6 недель), поэтому месяцы с 5 неделями не сжимают плашку: она растёт только вниз при добавлении контента. Тесты monthCells обновлены под инвариант. Диалог срока: «Снять срок» переехало из футера в тело (отдельное осознанное действие с пояснением), а в футере остались «Отмена» (слева, просто закрывает) и «Сохранить». Раньше «Снять срок» + «Готово» выглядели как одно действие в два клика и путали: снятие срока применяется сразу. LOCAL-DEV-DEFAULT (временно, перед мержем в main вернуть): install.sh и server-manager.ts по умолчанию берут dev вместо main, чтобы свежая локальная установка и кнопка «Обновить Xrayebator» сразу тянули новые фичи, а не релиз. Помечено комментариями в обоих местах.
Bypass удалён целиком (GUI, CLI, меню, тесты, доки). Причина: дефолтный
config.json уже содержит catch-all {network: tcp,udp → direct}, поэтому без
каскада bypass — no-op: правило domain→direct ведёт в тот же freedom-outbound,
что и catch-all. Проверено экспериментом на реальной логике роутинга. При
включённом каскаде эффект есть, но клиенты (HAPP/Hiddify) умеют собственный
обход, так что серверная реализация дублирует клиентскую и создаёт лишнюю
нагрузку на код. Удалено 19 функций, пункт меню 7 (меню перенумеровано 1-9),
CLI bypass_*, bundle-группы, миграция первого запуска, GUI-секция, i18n-ключи
(по 28 в каждой локали) и 10 CSS-классов; bypass*-имена остатков переименованы
(bypassRow → inlineRow и т.п.).
410 Gone: subhttp не отдаёт маршруты профилю с истёкшим/отключённым сроком —
клиент видит «срок вышел» вместо рабочего на вид конфига, который всё равно не
подключится (uuid снят с inbound'ов). Проверяются и флаг expire_disabled, и сам
срок (таймер мог не отработать — окно до 10 минут). Тело «Profile expired or
disabled» с точным content-length. Новая миграция subhttp_expired_410_2026
регенерирует heredoc-обработчик на существующих установках (main_menu+quickstart,
parity-тест), иначе 410 не появился бы без переустановки.
Календарь: сетка занимает столько строк, сколько нужно месяцу (5 или 6) — пустого
полотна внизу нет, как просил пользователь. Вверх ничего не дёргается: шапка со
стрелками и строка дней недели имеют фиксированную высоту, поэтому курсор не
ищет новое положение при листании. Тесты обновлены (13/13).
Валидация: 27/27 (удалён test-bypass-cli.sh, добавлены проверки 410 в
test-profile-revoke-expire-cli.sh), typecheck чистый, vitest 102/103.
Пользователь решил: раз фича была реализована — оставить её в проекте, но своими силами её не развивать (это задача для другого человека). Возвращено состояние 67764d1 — до наших сегодняшних доработок. Что вернулось (17 функций, побайтово идентичны прежней версии — сверено diff): меню пункт 7 «Управление обходом VPN» (меню снова 1-10), миграция первого запуска bypass_routing_2026 в main_menu+quickstart, helper'ы _bypass_list_current / _apply_bypass_rule / _bypass_remove_rule / _bypass_reset_all, группы доменов _bypass_bundle_groups, интерактивные обёртки, CLI-секция (bypass_command с list/add/remove/reset/bundle) и диспетчер, test-bypass-cli.sh, доки. Что НЕ возвращалось (наши доработки, пользователю не нужны): - bypass groups (JSON-вывод состава групп) и bypass unbundle; - GUI-секция в Server Settings, BypassManager, IPC-каналы, preload, типы, i18n. Проверено: 17/17 функций и главное меню идентичны оригиналу, 28/28 validation (включая test-bypass-cli и test-main-menu-numbering на 10 пунктов), typecheck чистый.
…соты) Причина: у .day стоял aspect-ratio: 1 при фиксированной высоте строки 30px. Ширина колонки ~47px, поэтому выделенный день растягивался до 47px по высоте внутри 30px-строки и вылезал за рамку календаря. Проявлялось именно на выделенных ячейках (сегодня/выбранная) и в последней строке — как на скриншоте. Размер теперь задаёт сетка (grid-auto-rows: 34px), а ячейка лишь заполняет её (width/height: 100%) — ни в одной вариации количества недель выйти за рамку нельзя. Строка стала 34px вместо 30px: дни визуально просторнее, и строка «Выбрано: …» под календарём больше не наезжает на сетку. Добавлен overflow: hidden как страховка от будущих переполнений. Проверено: typecheck, vitest (календарь 13/13), CSS-классы 102/102 без неиспользуемых.
…rent_branch) Дефолт 'dev' был временным — для ручного перевода сервера на новые фичи. Теперь сервер закрепил ветку сам (`xrayebator update dev` пишет /usr/local/etc/xray/.current_branch), поэтому дефолт возвращён к main: - install.sh: свежая установка снова ставится с main (релизной ветки); - server-manager.ts: main — только фолбэк для серверов, где ветка ещё не закреплена; на нашем сервере в .current_branch лежит dev, так что кнопка «Обновить Xrayebator» тянет dev и фичи не пропадают (проверено на живом сервере: имитация кнопки → ветка dev, profile-revoke/expire/410 на месте). Проверено на сервере 2.27.13.17 (Debian 13): таймер сроков active, истёкший срок отдаёт 410, снятие срока — снова 200, сервисы xray и xrayebator-sub active.
Пользователь просил вернуть «яблочную» квадратную геометрию ячеек (она была в первой версии), убрав при этом баг с выходом за рамку и прыжки стрелок. Квадраты: сторона задана переменной --day-size (44px) вместо aspect-ratio внутри жёсткого grid-auto-rows. Именно конфликт «квадрат (47px) внутри строки фиксированной высоты (30-34px)» давал выход за рамку, который видел пользователь: выделенный день растягивался и вылезал за границу календаря. Теперь высоту строки определяет сам квадрат, поэтому переполнения нет ни при 5 неделях, ни при 6. Квадраты центрируются в колонках (justify-items), общий бокс календаря оставлен как был — ограничение ширины не вводилось. Шестая неделя: резерв высоты убран (min-height удалён) — месяц с 5 неделями занимает 5 строк, пустой зоны внизу нет. Чтобы верхняя часть при этом не сдвигалась, оба диалога с календарём получили placement="top": диалог прижат к верху, поэтому добавление шестой недели раскрывается ВНИЗ и не двигает шапку со стрелками и уже показанные дни. Проверено: typecheck чистый, сборка electron-vite проходит, vitest 102/103 (единственный красный — предсуществующий shell-command с /bin/sh под Windows).
Досинхронизация под фичи, добавленные в этой серии: - security (ru/en/zh): в список серверных гарантий добавлено, что профиль с истёкшим или отключённым сроком отдаёт 410 Gone (Profile expired or disabled) и не выдаёт маршруты вовсе — даже если клиент ещё не обновил подписку; уточнено, что полный revoke меняет и uuid, поэтому скачанные конфиги перестают подключаться; - troubleshooting (ru/en/zh): новый раздел «подписка возвращает 410, хотя профиль выглядит рабочим» — как отличить истечение срока от stale-портов, какие поля смотреть (.expire_disabled/.expire), как продлить или снять срок командой и что продление возвращает тот же uuid (переимпорт подписки не нужен); - architecture (ru/en/zh): в структуре /usr/local/etc/xray профиль описан как «метаданные, токен подписки, срок действия (.expire)». Ранее в этой серии уже были обновлены справочник CLI, desktop-gui и CHANGELOG — здесь закрыты оставшиеся пробелы. Проверено: 28/28 validation.
Вопрос пользователя «какое именно ВРЕМЯ ставится для окончания» вскрыл три
дефекта в одной строке логики.
1. Дата без времени означала НАЧАЛО суток. Выбирая «до 30 сентября», владелец
получал отключение ровно в 00:00 30-го — на сутки раньше ожидаемого, а
последним рабочим днём оказывалось 29-е. Теперь дата трактуется
включительно: срок истекает в 23:59:59 выбранного дня. Дата со временем
(«2026-09-30 15:45») по-прежнему использует указанное время как есть.
2. GUI показывал дату через toISOString (UTC-срез). Сервер пишет expire как
конец дня в СВОЕЙ зоне, поэтому для Москвы/Хельсинки выбранное 30 сентября
отображалось как 29-е — ровно тот симптом, который видел пользователь.
describeExpire переведён на локальные компоненты даты.
3. Восьмеричная ловушка bash: в `[[ "${BASH_REMATCH[2]}" -ge 1 ]]` строка "09"
читается как невалидное восьмеричное число, и валидация падала с
«value too great for base». То есть ЛЮБАЯ дата с месяцем/днём/часом 08 или 09
не принималась — включая весь сентябрь. Скрывалось тем, что тесты
использовали январь ("01"). Исправлено префиксом 10#.
Тесты: isFutureDate теперь считает срок по концу дня (сегодняшняя дата — валидный
выбор), добавлена регрессия на все проблемные разряды 08/09 и на UTC-сдвиг
отображения. 28/28 validation, 105/106 vitest (единственный красный —
предсуществующий shell-command с /bin/sh под Windows), typecheck чистый.
MSG"
Синхронизированы CLI-справочник, архитектура, описание GUI и troubleshooting: дата без времени действует весь выбранный день и истекает в 23:59:59 по локальным часам сервера; явно переданное время также интерпретируется в зоне сервера. CHANGELOG дополнен гарантией 410 для просроченных/отключённых профилей и исправлениями срока: предыдущая дата из-за UTC-среза, принятие полей 08/09 в десятичном формате. CLAUDE.md отражает ту же семантику и актуальные 28 validation-тестов.
Epoch задаёт абсолютный момент, но его календарный день различается в зоне VPS и зоне Electron-компьютера. Для срока 30.09.2030 23:59:59 на сервере UTC-7 тот же epoch уже относится к 01.10 на клиенте UTC+14; форматирование epoch на клиенте поэтому показывало неверный день. `profiles` теперь отдаёт `expire_date` — YYYY-MM-DD, вычисленный по серверным часам, вместе с прежним `expire` epoch для enforcement. GUI использует server local date в чипе, при открытии редактора и в текущем значении; старые сервера без поля сохраняют прежний fallback. Добавлены Vitest и Bash validation regressions на UTC-7 сервер / UTC+14 клиент. Проверка JSON контрактная: клиент должен получить именно дату, которую считает сервер, без смены timezone процесса. Локальный timezone harness подтвердил старый результат 01.10 и новый `expire_date: 2030-09-30`.
README, справочник CLI, архитектура, desktop-gui и CHANGELOG во всех трёх языках объясняют, что `profiles` возвращает epoch `expire` для enforcement и серверную календарную дату `expire_date` для отображения. Это важно, когда VPS и рабочий компьютер находятся по разные стороны даты из-за разницы зон. CLAUDE.md обновлён по контракту `profiles`; Unreleased changelog уточняет, что GUI берёт серверную дату, а regression покрывает сервер UTC-7 / клиент UTC+14.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Резюме
Revoke подписки, сроки действия профилей с серверным принуждением и календарь выбора даты — функции, которые в прошлом PR (#26) остались за границей GUI, плюс исправления по итогам живой проверки на VPS. Дата без времени действует включительно до
23:59:59по локальным часам сервера; явно заданное время трактуется в той же зоне. Сервер возвращаетexpire_dateв своей временной зоне: GUI не сдвигает дату, даже если сервер в UTC−7, а компьютер пользователя в UTC+14 (этот контрпример закреплён регрессионным тестом). Исправлен отказ дат и времени с ведущими нулями (08/09), например для сентября и08:09.devсинхронизирован с актуальнойmain(ec44f10): вся история main влита в dev, merge-tree конфликтов не даёт. На HEADeb58d85: PR содержит 30 коммитов и 44 файла;mainне изменялся.Что внутри
1. Revoke подписки в GUI (
profile-revoke)Кнопка у профиля открывает меню с двумя режимами:
sub_token; старая URL умирает, маршруты и ключи те же (обёртка логики TUI-пункта 2, рестарт subhttp не нужен — handler читает токен на каждый запрос);Порядок транзакции важен: сначала config +
safe_restart_xray, затем profile JSON; сбой записи профиля откатывает config из бэкапа транзакции (иначе uuid в конфиге и профиле разъедутся).2. Срок действия профиля с серверным принуждением
profile-create --expire ДАТА,profile-expire --expire ДАТА|epoch|none,expire-check;.expire(epoch-секунды) в profile JSON;profilesотдаётexpire, серверную календарную датуexpire_dateиexpire_disabled, а такжеexpire_supported— чтобы GUI отличал «профиль бессрочный» от «сервер старой версии, сроков не умеет».expire_dateне даёт GUI сдвигать отображаемую дату, если ПК и VPS в разных часовых зонах; регрессия проверяет сервер в UTC−7 и клиент в UTC+14;23:59:59по локальным часам сервера; явно указанное время тоже трактуется в часовом поясе сервера;xrayebator-expire.timerкаждые 10 минут вызываетxrayebator expire-checkи снимает клиента с inbound'ов по истечении срока; продление возвращает того же клиента (тот же uuid) — переимпорт подписки не нужен;expire_timer_2026(main_menu + quickstart, parity-тест),uninstall.shего снимает;Инварианты, заложенные осознанно:
.expire_clientsи возвращается при продлении;expire-checkидемпотентен: без изменений не перезапускает Xray (флаг.expire_disabled— маркер, а не снимок).3. Истёкший профиль отдаёт
410 Gonesubhttp не выдаёт маршруты профилю с истёкшим или отключённым сроком — клиент видит «срок вышел» вместо рабочего на вид конфига, который всё равно не подключится. Проверяются и флаг, и сам срок: таймер ходит раз в 10 минут, и без второй проверки был бы промежуток, когда срок прошёл, а маршруты ещё отдаются. Новая миграция
subhttp_expired_410_2026регенерирует heredoc-обработчик на существующих установках.4. Календарь выбора даты (GUI)
Свой компонент вместо системного
input[type=date]: в теме приложения, прошлые даты недоступны (сервер срок в прошлом всё равно отклонит), листание назад ограничено месяцем с нижней границей, сегодняшний день подсвечен. Квадратные боксы дат («яблочная» геометрия): сторона задана переменной--day-size, поэтому переполнения нет ни при 5 неделях, ни при 6. Шестая неделя раскрывается вниз и не сдвигает верх — диалоги прижаты к верху черезplacement="top".5. Диагностика CLI-падений
Новый
src/main/core/cli-failure.ts: причина достаётся из stderr, а если он пуст — из stdout. Менеджер печатает «Неизвестная команда» именно в stdout, поэтому раньше пользователь видел пустое «код 1: ». Плюс подсказка «сервер старой версии — обновите» при неизвестной команде.6. Ловушка с веткой обновления
xrayebator update <ветка>заменял скрипт, но не записывал.current_branch, а GUI-кнопка «Обновить Xrayebator» читает именно этот файл и по умолчанию берётmain. Итог: обновив сервер на dev, пользователь получал откат на релиз при следующем обновлении из GUI, и новые команды молча исчезали. Теперь ветка закрепляется после успешной замены скрипта, имя ветки валидируется до подстановки в URL. Регрессия закрыта тестомtest-update-branch-pinning.sh.7. Мелкие исправления UX
main; сервер остаётся наdevчерез.current_branch.bypass: итоговое состояние не меняется
В ходе работы bypass сначала получил
groups/unbundleи GUI-секцию, затем (после разбора семантики роутинга) это было откачено: без каскада правилоdomain → directведёт в тот же freedom-outbound, что и встроенный catch-all, поэтому фича в GUI бесполезна. Вdevbypass ровно такой же, как вmain: те же 16 функций, то же меню (пункт 7, нумерация 1–10), GUI-части нет ни там, ни там. Проверено побайтовым сравнением всех bypass-функций и сверкой меню. Доработкиgroups/unbundleв этой ветке не остались.Проверки
bash -nдляxrayebator,install.sh,update.sh,uninstall.sh;test-bypass-cli,test-main-menu-numbering,test-quickstart-migration-parity, новыеtest-profile-revoke-expire-cliиtest-update-branch-pinning);npm run typecheck— чисто;vitest— 106/107: единственный красныйshell-command.test.tsвызывает/bin/sh, которого нет в Windows; остальные 106 тестов проходят, включая 24/24 теста срока и календаря, в том числе сервер UTC−7 / клиент UTC+14. На Linux CI весь test job проходит;npm run build(electron-vite) — проходит;410с теломProfile expired or disabled, снятие срока → снова200; таймерxrayebator-expire.timerактивен; обновление с закреплённой ветки фичи не сбрасывает.Документация синхронизирована
README (ru/en/zh) кратко перечисляет возможности Server Settings;
docs/desktop-gui.md,docs/{configuration,architecture,security,troubleshooting}.mdи русская/китайская версии описывают GUI, команды, включительное время срока, timezone-контрактexpire_dateи 410.CHANGELOG.mdсодержит user-facing изменения,CLAUDE.md— CLI-контракт и 28 validation-тестов.Оговорки
devчерез.current_branch— на стабильностьmainэто не влияет, дефолт в коде осталсяmain;gui-legacyне тронут.