Skip to content

Commit 493e856

Browse files
committed
Merge branch 'dev'
2 parents 8643b08 + d758c83 commit 493e856

14 files changed

Lines changed: 2134 additions & 97 deletions

File tree

index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,17 @@ function setupCronJobs() {
742742
logger.error(`[Cron] Monthly maintenance failed: ${error.message}`);
743743
}
744744
});
745+
746+
// Flush yesterday's UA client stats from Redis to MongoDB at 00:15
747+
cron.schedule('15 0 * * *', async () => {
748+
try {
749+
const uaStatsService = require('./src/services/uaStatsService');
750+
await uaStatsService.flushYesterday();
751+
await uaStatsService.cleanup();
752+
} catch (error) {
753+
logger.error(`[Cron] UA stats flush failed: ${error.message}`);
754+
}
755+
});
745756

746757
// Clean old logs daily at 3:00
747758
cron.schedule('0 3 * * *', () => {

src/locales/en.json

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,78 @@
748748
"subBtnLabelTitle": "Button label",
749749
"subBtnUrlTitle": "URL — placeholders: {url}, {url_b64}, {url_encoded}",
750750
"subPickIcon": "Choose icon",
751-
"subDeleteBtn": "Remove"
751+
"subDeleteBtn": "Remove",
752+
"happTitle": "HAPP Integration",
753+
"happDesc": "Settings delivered exclusively to HAPP clients via HTTP headers on every subscription update.",
754+
"happAnnounce": "Announcement",
755+
"happAnnounceHint": "Short message shown to users inside the app (max 200 characters). Non-ASCII text (e.g. Cyrillic) is encoded automatically.",
756+
"happAnnounceCounter": "{n}/200",
757+
"happAdvancedToggle": "Advanced settings (requires Provider ID)",
758+
"happAdvancedHint": "Fill in HAPP Provider ID in the subscription settings above to enable advanced HAPP features.",
759+
"happHideSettings": "Hide server configuration",
760+
"happHideSettingsHint": "Users cannot view or copy server connection details from the app.",
761+
"happNotifyExpire": "Notify before expiration",
762+
"happNotifyExpireHint": "Push notification 3 days before the subscription expires.",
763+
"happAlwaysHwid": "Force hardware ID (HWID)",
764+
"happAlwaysHwidHint": "User cannot disable HWID transmission in app settings.",
765+
"happPingType": "Ping method",
766+
"happPingTypeHint": "How the app measures server latency.",
767+
"happPingTypeAuto": "Auto (app default)",
768+
"happPingTypeProxy": "Via Proxy (GET)",
769+
"happPingTypeProxyHead": "Via Proxy (HEAD)",
770+
"happPingTypeTcp": "TCP",
771+
"happPingTypeIcmp": "ICMP",
772+
"happPingUrl": "Ping check URL",
773+
"happPingUrlHint": "URL used for proxy ping check. Only applies when ping method is Via Proxy.",
774+
"happColorProfile": "Color theme (iOS)",
775+
"happColorProfileHint": "Custom color theme JSON. Create a theme in the HAPP iOS app (long-press Appearance Theme), export to clipboard, then paste here.",
776+
"happColorPresetViolet": "Violet",
777+
"happColorPresetTurquoise": "Turquoise",
778+
"happColorPresetClear": "Clear",
779+
"routingTitle": "Client-side Routing (Split Tunneling)",
780+
"routingDesc": "Configure which websites and services your users access directly (bypassing VPN) and which go through the VPN tunnel.",
781+
"routingEnabled": "Enable routing rules",
782+
"routingEnabledHint": "When disabled, subscriptions are generated without routing rules — all traffic goes through VPN.",
783+
"routingHowItWorks": "How does it work?",
784+
"routingHowItWorksDesc": "Routing rules are embedded into subscription configs delivered to VPN clients. When a user opens a website or app, the client checks the destination against these rules:<br>• <strong>direct</strong> — traffic goes directly, bypassing the VPN (e.g. local banking, government services).<br>• <strong>block</strong> — traffic is dropped (e.g. ads, trackers).<br>• Everything else goes through the VPN tunnel as usual.<br><br>Rules also configure <strong>Split DNS</strong>: domains matching direct rules are resolved via the domestic DNS server, while all other queries go through the encrypted remote DNS.",
785+
"routingPresets": "Presets",
786+
"routingPresetsHint": "Presets are additive — click multiple presets to combine them. For example, \"Bypass RU\" + \"Block Ads\" will add both rule sets. Duplicate rules are skipped automatically.",
787+
"presetBypassRu": "Bypass RU",
788+
"presetBypassRuDesc": "Russian sites & IPs direct",
789+
"presetBypassCn": "Bypass CN",
790+
"presetBypassCnDesc": "Chinese sites & IPs direct",
791+
"presetBlockAds": "Block Ads",
792+
"presetBlockAdsDesc": "Block ad & tracker networks",
793+
"presetBypassLan": "Bypass LAN",
794+
"presetBypassLanDesc": "Local network direct",
795+
"presetClear": "Clear all",
796+
"presetClearDesc": "Remove all rules",
797+
"routingRules": "Rules",
798+
"routingRulesHint": "Rules are applied top to bottom. First match wins. Disabled rules are skipped.",
799+
"routingNoRules": "No rules yet. Add manually or pick a preset above.",
800+
"routingAddRule": "Add rule",
801+
"routingAction": "Action",
802+
"routingType": "Type",
803+
"routingValue": "Value",
804+
"routingComment": "Comment",
805+
"routingEnabled2": "On",
806+
"routingActionDirect": "direct",
807+
"routingActionBlock": "block",
808+
"routingTypeDomainSuffix": "domain suffix",
809+
"routingTypeDomainKeyword": "keyword",
810+
"routingTypeDomain": "domain",
811+
"routingTypeGeosite": "geosite",
812+
"routingTypeGeoip": "geoip",
813+
"routingTypeIpCidr": "IP CIDR",
814+
"routingDns": "Split DNS",
815+
"routingDnsHint": "Domains matching \"direct\" rules are resolved via the domestic DNS. Everything else is resolved through the remote (encrypted) DNS via proxy tunnel.",
816+
"routingDnsDomestic": "Domestic DNS",
817+
"routingDnsDomesticHint": "Plain UDP DNS for direct domains (e.g. Yandex DNS: 77.88.8.8)",
818+
"routingDnsRemote": "Remote DNS",
819+
"routingDnsRemoteHint": "Encrypted DNS for proxied traffic (e.g. Cloudflare DoT: tls://1.1.1.1)",
820+
"routingClientSupport": "Client compatibility",
821+
"routingClientSupportDesc": "<strong>Full support:</strong> HAPP (native routing protocol), Clash Meta / Stash (YAML rules).<br><strong>Partial support:</strong> Hiddify / SFA / NekoBox (sing-box JSON — some clients may override rules with their own templates).<br><strong>No support:</strong> v2rayNG, Streisand, Shadowrocket — these clients use URI subscriptions which cannot carry routing rules. Users need to configure routing manually.",
822+
"clearRulesConfirm": "Remove all routing rules?"
752823
},
753824
"setup": {
754825
"title": "Initial Setup",
@@ -877,7 +948,13 @@
877948
"hourChange": "hour change",
878949
"h": "h",
879950
"d": "d",
880-
"hint": "Data updates every 5 minutes. Select a period to view history."
951+
"hint": "Data updates every 5 minutes. Select a period to view history.",
952+
"clientsTitle": "VPN Clients",
953+
"clientsDesc": "Unique users per VPN client app, based on subscription requests. Counted by unique subscription token per day.",
954+
"clientName": "Client",
955+
"uniqueUsers": "unique users",
956+
"percentage": "%",
957+
"clientsNoData": "No subscription requests recorded yet"
881958
},
882959
"apiKeys": {
883960
"title": "API Keys",

src/locales/ru.json

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,78 @@
748748
"subBtnLabelTitle": "Название кнопки",
749749
"subBtnUrlTitle": "URL — переменные: {url}, {url_b64}, {url_encoded}",
750750
"subPickIcon": "Выбрать иконку",
751-
"subDeleteBtn": "Удалить"
751+
"subDeleteBtn": "Удалить",
752+
"happTitle": "Интеграция HAPP",
753+
"happDesc": "Настройки, которые доставляются исключительно HAPP-клиентам через HTTP-заголовки при каждом обновлении подписки.",
754+
"happAnnounce": "Объявление",
755+
"happAnnounceHint": "Короткое сообщение пользователям внутри приложения (макс. 200 символов). Кирилица и другие не-ASCII символы кодируются автоматически.",
756+
"happAnnounceCounter": "{n}/200",
757+
"happAdvancedToggle": "Расширенные настройки (требуется Provider ID)",
758+
"happAdvancedHint": "Заполните HAPP Provider ID в настройках подписки выше, чтобы активировать расширенные функции HAPP.",
759+
"happHideSettings": "Скрыть конфигурацию серверов",
760+
"happHideSettingsHint": "Пользователи не смогут просматривать или копировать данные подключения из приложения.",
761+
"happNotifyExpire": "Уведомлять до истечения",
762+
"happNotifyExpireHint": "Push-уведомление за 3 дня до окончания подписки.",
763+
"happAlwaysHwid": "Принудительный HWID",
764+
"happAlwaysHwidHint": "Пользователь не сможет отключить передачу идентификатора устройства в настройках приложения.",
765+
"happPingType": "Метод пинга",
766+
"happPingTypeHint": "Как приложение измеряет задержку серверов.",
767+
"happPingTypeAuto": "Авто (по умолчанию)",
768+
"happPingTypeProxy": "Через прокси (GET)",
769+
"happPingTypeProxyHead": "Через прокси (HEAD)",
770+
"happPingTypeTcp": "TCP",
771+
"happPingTypeIcmp": "ICMP",
772+
"happPingUrl": "URL для пинга",
773+
"happPingUrlHint": "URL для проверки через прокси. Применяется только при методе «Через прокси».",
774+
"happColorProfile": "Цветовая тема (iOS)",
775+
"happColorProfileHint": "JSON кастомной темы. Создайте тему в HAPP iOS (долгое нажатие на «Внешний вид»), экспортируйте в буфер обмена и вставьте сюда.",
776+
"happColorPresetViolet": "Фиолетовая",
777+
"happColorPresetTurquoise": "Бирюзовая",
778+
"happColorPresetClear": "Очистить",
779+
"routingTitle": "Клиентская маршрутизация (Split Tunneling)",
780+
"routingDesc": "Настройте, какие сайты и сервисы пользователи открывают напрямую (минуя VPN), а какие — через VPN-туннель.",
781+
"routingEnabled": "Включить правила маршрутизации",
782+
"routingEnabledHint": "Если выключено — подписки генерируются без routing rules, весь трафик идёт через VPN.",
783+
"routingHowItWorks": "Как это работает?",
784+
"routingHowItWorksDesc": "Правила маршрутизации встраиваются в конфигурации подписок и доставляются VPN-клиентам. Когда пользователь открывает сайт или приложение, клиент проверяет адрес по этим правилам:<br>• <strong>direct</strong> — трафик идёт напрямую, минуя VPN (например, банки, госуслуги).<br>• <strong>block</strong> — трафик блокируется (например, реклама, трекеры).<br>• Всё остальное идёт через VPN-туннель как обычно.<br><br>Также настраивается <strong>Split DNS</strong>: домены из direct-правил резолвятся через внутренний DNS, а все остальные запросы — через зашифрованный удалённый DNS.",
785+
"routingPresets": "Пресеты",
786+
"routingPresetsHint": "Пресеты наслаиваются — нажимайте несколько, чтобы комбинировать. Например, «Bypass RU» + «Block Ads» добавит оба набора правил. Дубликаты пропускаются автоматически.",
787+
"presetBypassRu": "Bypass RU",
788+
"presetBypassRuDesc": "Российские сайты и IP напрямую",
789+
"presetBypassCn": "Bypass CN",
790+
"presetBypassCnDesc": "Китайские сайты и IP напрямую",
791+
"presetBlockAds": "Block Ads",
792+
"presetBlockAdsDesc": "Блокировать рекламу и трекеры",
793+
"presetBypassLan": "Bypass LAN",
794+
"presetBypassLanDesc": "Локальная сеть напрямую",
795+
"presetClear": "Очистить всё",
796+
"presetClearDesc": "Удалить все правила",
797+
"routingRules": "Правила",
798+
"routingRulesHint": "Правила применяются сверху вниз. Первое совпадение побеждает. Выключенные правила пропускаются.",
799+
"routingNoRules": "Правил пока нет. Добавьте вручную или выберите пресет выше.",
800+
"routingAddRule": "Добавить правило",
801+
"routingAction": "Действие",
802+
"routingType": "Тип",
803+
"routingValue": "Значение",
804+
"routingComment": "Комментарий",
805+
"routingEnabled2": "Вкл",
806+
"routingActionDirect": "direct",
807+
"routingActionBlock": "block",
808+
"routingTypeDomainSuffix": "суффикс домена",
809+
"routingTypeDomainKeyword": "ключевое слово",
810+
"routingTypeDomain": "домен",
811+
"routingTypeGeosite": "geosite",
812+
"routingTypeGeoip": "geoip",
813+
"routingTypeIpCidr": "IP CIDR",
814+
"routingDns": "Split DNS",
815+
"routingDnsHint": "Домены из direct-правил резолвятся через внутренний DNS. Всё остальное — через зашифрованный удалённый DNS через прокси-туннель.",
816+
"routingDnsDomestic": "Внутренний DNS",
817+
"routingDnsDomesticHint": "Обычный UDP DNS для direct-доменов (например Яндекс DNS: 77.88.8.8)",
818+
"routingDnsRemote": "Удалённый DNS",
819+
"routingDnsRemoteHint": "Зашифрованный DNS для проксируемого трафика (например Cloudflare DoT: tls://1.1.1.1)",
820+
"routingClientSupport": "Совместимость клиентов",
821+
"routingClientSupportDesc": "<strong>Полная поддержка:</strong> HAPP (встроенный протокол маршрутизации), Clash Meta / Stash (YAML-правила).<br><strong>Частичная поддержка:</strong> Hiddify / SFA / NekoBox (sing-box JSON — некоторые клиенты могут заменять правила своими шаблонами).<br><strong>Не поддерживается:</strong> v2rayNG, Streisand, Shadowrocket — эти клиенты используют URI-подписки, которые не могут содержать routing rules. Пользователям нужно настраивать маршрутизацию вручную.",
822+
"clearRulesConfirm": "Удалить все правила маршрутизации?"
752823
},
753824
"setup": {
754825
"title": "Первоначальная настройка",
@@ -877,7 +948,13 @@
877948
"hourChange": "изм. за час",
878949
"h": "ч",
879950
"d": "д",
880-
"hint": "Данные обновляются каждые 5 минут. Выберите период для просмотра истории."
951+
"hint": "Данные обновляются каждые 5 минут. Выберите период для просмотра истории.",
952+
"clientsTitle": "VPN-клиенты",
953+
"clientsDesc": "Уникальные пользователи по приложениям VPN, на основе запросов подписки. Считается уникальный токен подписки в день.",
954+
"clientName": "Клиент",
955+
"uniqueUsers": "уникальных пользователей",
956+
"percentage": "%",
957+
"clientsNoData": "Запросы подписки ещё не зафиксированы"
881958
},
882959
"apiKeys": {
883960
"title": "API Ключи",

src/models/settingsModel.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,20 @@ const settingsSchema = new mongoose.Schema({
4545

4646
backup: {
4747
enabled: { type: Boolean, default: false },
48-
intervalHours: { type: Number, default: 24 }, // интервал в часах
49-
keepLast: { type: Number, default: 7 }, // сколько хранить локально
50-
lastBackup: { type: Date, default: null }, // время последнего бэкапа
48+
intervalHours: { type: Number, default: 24 }, // interval in hours
49+
keepLast: { type: Number, default: 7 }, // how many to keep locally
50+
lastBackup: { type: Date, default: null }, // last backup timestamp
5151

52-
// S3 настройки (опционально)
52+
// S3 settings (optional)
5353
s3: {
5454
enabled: { type: Boolean, default: false },
55-
endpoint: { type: String, default: '' }, // для MinIO и подобных
55+
endpoint: { type: String, default: '' }, // for MinIO and similar
5656
region: { type: String, default: 'us-east-1' },
5757
bucket: { type: String, default: '' },
58-
prefix: { type: String, default: 'backups' }, // префикс в bucket
58+
prefix: { type: String, default: 'backups' }, // prefix in bucket
5959
accessKeyId: { type: String, default: '' },
6060
secretAccessKey: { type: String, default: '' },
61-
keepLast: { type: Number, default: 30 }, // сколько хранить в S3
61+
keepLast: { type: Number, default: 30 }, // how many to keep in S3
6262
},
6363
},
6464

@@ -86,6 +86,34 @@ const settingsSchema = new mongoose.Schema({
8686
}],
8787
default: [],
8888
},
89+
happ: {
90+
announce: { type: String, default: '' },
91+
hideSettings: { type: Boolean, default: false },
92+
notifyExpire: { type: Boolean, default: false },
93+
alwaysHwid: { type: Boolean, default: false },
94+
pingType: { type: String, enum: ['', 'proxy', 'proxy-head', 'tcp', 'icmp'], default: '' },
95+
pingUrl: { type: String, default: '' },
96+
colorProfile: { type: String, default: '' },
97+
},
98+
},
99+
100+
routing: {
101+
enabled: { type: Boolean, default: false },
102+
rules: {
103+
type: [{
104+
_id: false,
105+
action: { type: String, enum: ['direct', 'block'], default: 'direct' },
106+
type: { type: String, enum: ['domain_suffix', 'domain_keyword', 'domain', 'geosite', 'geoip', 'ip_cidr'] },
107+
value: { type: String },
108+
comment: { type: String, default: '' },
109+
enabled: { type: Boolean, default: true },
110+
}],
111+
default: [],
112+
},
113+
dns: {
114+
domestic: { type: String, default: '77.88.8.8' },
115+
remote: { type: String, default: 'tls://1.1.1.1' },
116+
},
89117
},
90118

91119
}, { timestamps: true });

0 commit comments

Comments
 (0)