Skip to content

Revoke подписки, сроки действия профилей с серверным принуждением, 410 для истёкших и календарь - #27

Open
Ap3x0s wants to merge 30 commits into
mainfrom
dev
Open

Ap3x0s wants to merge 30 commits into
mainfrom
dev

Conversation

@Ap3x0s

@Ap3x0s Ap3x0s commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Резюме

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 конфликтов не даёт. На HEAD eb58d85: PR содержит 30 коммитов и 44 файла; main не изменялся.

Что внутри

1. Revoke подписки в GUI (profile-revoke)

Кнопка у профиля открывает меню с двумя режимами:

  • Только новая ссылка — перевыпуск sub_token; старая URL умирает, маршруты и ключи те же (обёртка логики TUI-пункта 2, рестарт subhttp не нужен — handler читает токен на каждый запрос);
  • Полный отзыв — новый токен и новый uuid во всех inbound'ах профиля: устройства, уже скачавшие конфиг, отключаются немедленно. Это единственный способ реально закрыть доступ по утёкшей в чат ссылке; второй шаг-подтверждение, после успеха показывается новая ссылка.

Порядок транзакции важен: сначала config + safe_restart_xray, затем profile JSON; сбой записи профиля откатывает config из бэкапа транзакции (иначе uuid в конфиге и профиле разъедутся).

2. Срок действия профиля с серверным принуждением

  • CLI: 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 по локальным часам сервера; явно указанное время тоже трактуется в часовом поясе сервера;
  • принуждение серверное: systemd-таймер xrayebator-expire.timer каждые 10 минут вызывает xrayebator expire-check и снимает клиента с inbound'ов по истечении срока; продление возвращает того же клиента (тот же uuid) — переимпорт подписки не нужен;
  • таймер ставится миграцией expire_timer_2026 (main_menu + quickstart, parity-тест), uninstall.sh его снимает;
  • GUI: чип «до ДД.ММ.ГГГГ» с подсветкой за 3 дня до срока и красной после, диалог с пресетами +7/+30/+90/+365 и снятием срока, поле срока при создании профиля.

Инварианты, заложенные осознанно:

  • при отключении inbound'ы остаются живыми — удаление пустого inbound и создание нового даёт новый reality shortId, и все выданные клиентам URL умерли бы; клиент снимается, а его объект (с flow) сохраняется в .expire_clients и возвращается при продлении;
  • expire-check идемпотентен: без изменений не перезапускает Xray (флаг .expire_disabled — маркер, а не снимок).

3. Истёкший профиль отдаёт 410 Gone

subhttp не выдаёт маршруты профилю с истёкшим или отключённым сроком — клиент видит «срок вышел» вместо рабочего на вид конфига, который всё равно не подключится. Проверяются и флаг, и сам срок: таймер ходит раз в 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 бесполезна. В dev bypass ровно такой же, как в main: те же 16 функций, то же меню (пункт 7, нумерация 1–10), GUI-части нет ни там, ни там. Проверено побайтовым сравнением всех bypass-функций и сверкой меню. Доработки groups/unbundle в этой ветке не остались.

Проверки

  • bash -n для xrayebator, install.sh, update.sh, uninstall.sh;
  • validation: 28/28 (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) — проходит;
  • сверка i18n: 127 ключей в каждой локали, все ключи из кода резолвятся; CSS-классов неиспользуемых нет;
  • живая проверка на Debian 13 VPS: срок в прошлом → подписка отдаёт 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 не тронут.

Ap3x0s and others added 30 commits September 25, 2026 00:43
Версия 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.
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