style: исправлены стили и форматирование кода
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 0s

This commit is contained in:
2026-05-08 18:23:56 +03:00
parent 8789496ae6
commit 1ed79c3a1e
8 changed files with 320 additions and 211 deletions

View File

@@ -2,32 +2,43 @@ async function request(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
'content-type': 'application/json',
"content-type": "application/json",
...(options.headers || {}),
},
});
const data = await response.json().catch(() => ({}));
if (!response.ok || (data && data.success === false)) {
throw new Error(data?.error || `Запрос ${url} завершился ошибкой ${response.status}`);
throw new Error(
data?.error || `Запрос ${url} завершился ошибкой ${response.status}`,
);
}
return data;
}
export const api = {
state: () => request('/api/state'),
config: () => request('/api/config'),
state: () => request("/api/state"),
config: () => request("/api/config"),
rules: {
get: () => request('/api/rules'),
save: (rules) => request('/api/rules', { method: 'PUT', body: JSON.stringify({ rules }) }),
get: () => request("/api/rules"),
save: (rules) =>
request("/api/rules", { method: "PUT", body: JSON.stringify({ rules }) }),
},
subscription: {
fetch: (url) => request('/api/subscription/fetch', { method: 'POST', body: JSON.stringify({ url }) }),
forget: () => request('/api/subscription', { method: 'DELETE' }),
fetch: (url) =>
request("/api/subscription/fetch", {
method: "POST",
body: JSON.stringify({ url }),
}),
forget: () => request("/api/subscription", { method: "DELETE" }),
},
apply: (selectedTag) => request('/api/apply', { method: 'POST', body: JSON.stringify({ selectedTag }) }),
apply: (selectedTag) =>
request("/api/apply", {
method: "POST",
body: JSON.stringify({ selectedTag }),
}),
singbox: {
stop: () => request('/api/singbox/stop', { method: 'POST' }),
restart: () => request('/api/singbox/restart', { method: 'POST' }),
clear: () => request('/api/singbox/clear', { method: 'POST' }),
stop: () => request("/api/singbox/stop", { method: "POST" }),
restart: () => request("/api/singbox/restart", { method: "POST" }),
clear: () => request("/api/singbox/clear", { method: "POST" }),
},
};

View File

@@ -9,7 +9,7 @@ function id(prefix) {
function template(name, outbound, fields) {
return {
id: id('tpl'),
id: id("tpl"),
name,
enabled: true,
outbound,
@@ -25,61 +25,103 @@ function template(name, outbound, fields) {
export const ruleTemplates = [
{
key: 'lol-direct',
label: 'League of Legends → direct',
description: 'Riot/LoL домены и порты — играть напрямую без VPN.',
key: "lol-direct",
label: "League of Legends → direct",
description: "Riot/LoL домены и порты — играть напрямую без VPN.",
build: () =>
template('League of Legends', 'direct', {
domainSuffixes: ['leagueoflegends.com', 'riotgames.com', 'riotcdn.net', 'dyn.riotcdn.net'],
ports: ['5000', '5223', '5222', '8088'],
template("League of Legends", "direct", {
domainSuffixes: [
"leagueoflegends.com",
"riotgames.com",
"riotcdn.net",
"dyn.riotcdn.net",
],
ports: ["5000", "5223", "5222", "8088"],
}),
},
{
key: 'discord-direct',
label: 'Discord/Vesktop → direct',
description: 'Discord voice/video и WebSocket напрямую.',
key: "discord-direct",
label: "Discord/Vesktop → direct",
description: "Discord voice/video и WebSocket напрямую.",
build: () =>
template('Discord', 'direct', {
domainSuffixes: ['discord.com', 'discord.gg', 'discord.media', 'discordapp.com', 'discordapp.net'],
ports: ['50000-65535'],
networks: ['udp'],
template("Discord", "direct", {
domainSuffixes: [
"discord.com",
"discord.gg",
"discord.media",
"discordapp.com",
"discordapp.net",
],
ports: ["50000-65535"],
networks: ["udp"],
}),
},
{
key: 'telegram-vpn',
label: 'Telegram → VPN',
description: 'Telegram через выбранный VPN outbound.',
key: "telegram-vpn",
label: "Telegram → VPN",
description: "Telegram через выбранный VPN outbound.",
build: () =>
template('Telegram', 'vpn', {
domainSuffixes: ['telegram.org', 't.me', 'telegram.me', 'telegra.ph', 'tdesktop.com'],
ipCidrs: ['149.154.160.0/20', '91.108.4.0/22', '91.108.8.0/22', '91.108.12.0/22', '91.108.16.0/22', '91.108.56.0/22'],
template("Telegram", "vpn", {
domainSuffixes: [
"telegram.org",
"t.me",
"telegram.me",
"telegra.ph",
"tdesktop.com",
],
ipCidrs: [
"149.154.160.0/20",
"91.108.4.0/22",
"91.108.8.0/22",
"91.108.12.0/22",
"91.108.16.0/22",
"91.108.56.0/22",
],
}),
},
{
key: 'youtube-vpn',
label: 'YouTube → VPN',
description: 'YouTube/Google Video через VPN.',
key: "youtube-vpn",
label: "YouTube → VPN",
description: "YouTube/Google Video через VPN.",
build: () =>
template('YouTube', 'vpn', {
domainSuffixes: ['youtube.com', 'youtu.be', 'ytimg.com', 'googlevideo.com', 'youtube-nocookie.com'],
template("YouTube", "vpn", {
domainSuffixes: [
"youtube.com",
"youtu.be",
"ytimg.com",
"googlevideo.com",
"youtube-nocookie.com",
],
}),
},
{
key: 'steam-direct',
label: 'Steam → direct',
description: 'Загрузка/обновления Steam напрямую.',
key: "steam-direct",
label: "Steam → direct",
description: "Загрузка/обновления Steam напрямую.",
build: () =>
template('Steam', 'direct', {
domainSuffixes: ['steampowered.com', 'steamcontent.com', 'steamcommunity.com', 'steamserver.net', 'cm.steampowered.com'],
template("Steam", "direct", {
domainSuffixes: [
"steampowered.com",
"steamcontent.com",
"steamcommunity.com",
"steamserver.net",
"cm.steampowered.com",
],
}),
},
{
key: 'ads-block',
label: 'Реклама → block',
description: 'Базовый набор рекламных доменов — заблокировать.',
key: "ads-block",
label: "Реклама → block",
description: "Базовый набор рекламных доменов — заблокировать.",
build: () =>
template('Реклама (block)', 'block', {
domainSuffixes: ['doubleclick.net', 'googlesyndication.com', 'googleadservices.com', 'adservice.google.com', 'adnxs.com'],
template("Реклама (block)", "block", {
domainSuffixes: [
"doubleclick.net",
"googlesyndication.com",
"googleadservices.com",
"adservice.google.com",
"adnxs.com",
],
}),
},
];

View File

@@ -1,6 +1,6 @@
export function formatBytes(value) {
if (!value) return '0 Б';
const units = ['Б', 'КБ', 'МБ', 'ГБ', 'ТБ'];
if (!value) return "0 Б";
const units = ["Б", "КБ", "МБ", "ГБ", "ТБ"];
let size = value;
let index = 0;
while (size >= 1024 && index < units.length - 1) {
@@ -11,9 +11,9 @@ export function formatBytes(value) {
}
export function formatRelative(iso) {
if (!iso) return '';
if (!iso) return "";
const ts = new Date(iso).getTime();
if (Number.isNaN(ts)) return '';
if (Number.isNaN(ts)) return "";
const diff = Math.max(0, Date.now() - ts);
const sec = Math.floor(diff / 1000);
if (sec < 60) return `${sec} с назад`;
@@ -26,6 +26,6 @@ export function formatRelative(iso) {
}
export function formatTime(iso) {
if (!iso) return '';
return new Date(iso).toLocaleTimeString('ru-RU', { hour12: false });
if (!iso) return "";
return new Date(iso).toLocaleTimeString("ru-RU", { hour12: false });
}

View File

@@ -1,17 +1,19 @@
// Простые валидаторы для полей правил роутинга. Возвращают массив ошибочных строк.
const IPV4 = /^((25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(25[0-5]|2[0-4]\d|[01]?\d?\d)$/;
const IPV4 =
/^((25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(25[0-5]|2[0-4]\d|[01]?\d?\d)$/;
const IPV6 = /^[0-9a-f:]+$/i;
const DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
const DOMAIN =
/^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
export function invalidCidrs(values) {
return (values || []).filter((value) => !isValidCidr(value));
}
export function isValidCidr(value) {
const trimmed = String(value || '').trim();
const trimmed = String(value || "").trim();
if (!trimmed) return false;
const [addr, mask] = trimmed.split('/');
const [addr, mask] = trimmed.split("/");
if (!addr) return false;
if (IPV4.test(addr)) {
@@ -19,7 +21,7 @@ export function isValidCidr(value) {
const m = Number(mask);
return Number.isInteger(m) && m >= 0 && m <= 32;
}
if (IPV6.test(addr) && addr.includes(':')) {
if (IPV6.test(addr) && addr.includes(":")) {
if (mask === undefined) return true;
const m = Number(mask);
return Number.isInteger(m) && m >= 0 && m <= 128;