Compare commits

...

52 commits

Author SHA1 Message Date
1aa31aa8d2 poetry update packages 2025-11-27 13:41:19 +05:00
8b936603cc +1 2025-11-27 13:17:05 +05:00
b77725b9f9 Fix front table invisible list 2025-11-27 13:11:15 +05:00
660fb66fc0 Alternative method scan subnet 2025-11-25 16:45:29 +05:00
a69c5ac5fb Delete hardcode ip address for scan 2025-11-25 14:12:24 +05:00
5fc8e1ce7e Package backend in bin onefile 2025-11-25 13:52:54 +05:00
11a1c6e392 Fallback for func check ip if scapy/arp not working 2025-11-24 16:59:36 +05:00
84adc5d941 repair translations 2025-11-19 15:03:02 +05:00
 Илья Шалонин
98581132dd Исправил locale на всех устройствах 2025-11-19 14:41:08 +05:00
40c98e68d7 websocket connect repair 2025-11-17 12:13:30 +05:00
a13636ca49 Edit depends call in i18n 2025-11-17 12:05:43 +05:00
9f0ab4ae5a Update export project requirements 2025-10-21 12:42:46 +05:00
752604b20b Утилита для упрощения генерации доков 2025-10-21 12:37:00 +05:00
ab38a0db50 Дополнение логики подключения к WiFi, если профиль оказывается битый 2025-10-21 12:27:01 +05:00
e1b027b301 Repair depends in wifi service 2025-10-21 11:13:56 +05:00
6a4ad48577 Repair docs (html, md). Update docstrings 2025-10-21 10:51:32 +05:00
e264750187 Unicode repair 2025-10-21 09:20:50 +05:00
Merelden
2b8eff58ac Some comments for config Sphinx 2025-10-14 16:38:38 +05:00
Merelden
81ff7959f7 Generate Sphinx docs in markdown 2025-10-14 16:11:12 +05:00
Merelden
bc4e9caacb Correcting small issues 2025-10-14 12:26:51 +05:00
Merelden
af1420dbde Sphinx generate docs 2025-10-14 12:17:13 +05:00
Merelden
5bc23e7eb8 Test 2025-10-14 11:44:44 +05:00
Merelden
8f48df2cf5 Покрытие кода docstrings 2025-10-14 11:42:57 +05:00
4e92aba2a2 Удалить SRV/storage/devices/devices.json 2025-08-12 12:43:43 +05:00
test
4286f28d1e Полная интернационализация 2025-08-11 13:56:43 +05:00
3315123029 poetry update 2025-07-17 10:49:49 +05:00
ab78185f08 Запрос на определение языка системы 2025-07-17 10:35:48 +05:00
0c8533257d Экспорт конкретный подсетей. Обновление локалей учитывая жти изменения 2025-07-16 13:48:34 +05:00
b1306ff66f Разделение сканирования на разные подсети. Хранение каждой из просканированных подсетей. Исправление багов с заметками и обновленные локали 2025-07-16 13:27:49 +05:00
c326edfb9a Удаление ненужных импортов локалей 2025-07-16 10:52:05 +05:00
ec054bbaad Единая интернационализация сервера без доменов 2025-07-15 15:12:01 +05:00
f0d5481dfe Изменение сканирования подсети и точек доступа wi-fi в связи с последними изменениями на сервере 2025-07-15 13:07:14 +05:00
990a7594fc Обновление сервиса для запуска gonets 2025-07-15 11:21:38 +05:00
35225b90a1 Обновленный перевод сервера 2025-07-15 11:18:33 +05:00
4215a62b6c Изменение языка единоразово для всех последующих запросов 2025-07-15 10:49:55 +05:00
6efbb4285f Обновление данных pyproject.toml 2025-07-14 17:36:06 +05:00
200d47754c Переключение языка по аргументу Accept-Language в запросе 2025-07-10 13:34:45 +05:00
44dab7402d Небольшие поправки 2025-07-09 13:16:34 +05:00
ba8cea01e7 Интернационализация для всех сервисов сервера 2025-07-08 15:57:42 +05:00
7491ce455d Небольшой рефакторинг кода 2025-07-08 15:08:32 +05:00
9ac52c8a8b Инициализация интернационализации. Перевод на en, ru для эндпоинтов api/v1 2025-07-08 15:02:39 +05:00
test
48e8551b0e Доработка мелочей и начало интернац. 2025-07-07 17:35:46 +05:00
486d46c467 Экспорт точек доступа и узлов подсети 2025-07-04 16:03:59 +05:00
test
e896a13f3c Delete Head 2025-07-04 16:01:31 +05:00
test
b997f554bc error 2025-07-04 15:59:14 +05:00
test
ca33a55375 Электрон проверка 2025-07-04 15:54:02 +05:00
26a364aad6 Реакт ошибка
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-07-04 15:27:40 +05:00
test
c423cf7172 Исправление бага с таблицей
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
2025-07-04 15:16:50 +05:00
1672f54e02 Merge branch 'main' of https://dev.gossys.dev/HybrydMain/gonets 2025-07-04 15:10:32 +05:00
61c526d3ee Эскпорт данных тест 2025-07-04 15:09:14 +05:00
test
ee350c1852 Добавление настроек и их функциональности
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
2025-07-04 15:07:41 +05:00
4257864b3d Нативное указание подсети для сканирования по умолчанию (192.168.1.0/24). Обработка ошибок при очистке данных 2025-07-04 13:48:38 +05:00
387 changed files with 49665 additions and 3405 deletions

3
.gitignore vendored
View file

@ -6,5 +6,6 @@ __pycache__
.wheels .wheels
.vendor .vendor
.python3-deb .python3-deb
SRV/storage/devices/devices.json SRV/storage/subnets/*
SRV/storage/wifi/wifi.json SRV/storage/wifi/wifi.json
build/

View file

@ -16,6 +16,7 @@ steps:
- python3 --version - python3 --version
- node -v - node -v
- npm -v - npm -v
raspberry
# ПОДГОТОВКА СПИСКА PYTHON ЗАВИСИМОСТЕЙ GONETS # ПОДГОТОВКА СПИСКА PYTHON ЗАВИСИМОСТЕЙ GONETS
- rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED - rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false) setHasInited(false)
initializationAttempt.current = 0; initializationAttempt.current = 0;
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`); const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
let connectionTimeout; let connectionTimeout;
setSocket(newSocket); setSocket(newSocket);

View file

@ -115,7 +115,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => { const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`); const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
testSocket.close(); testSocket.close();
resolve(false); resolve(false);

View file

@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { useIpStore } from './src/store'; import { useIpStore } from './src/store';
import { useEffect } from 'react'; import { useEffect } from 'react';
import {useCheckAdmin} from './src/utils/checkAdmin.ts' import { useCheckAdmin } from './src/utils/checkAdmin';
import { import {
ModalProvider, ModalProvider,
@ -11,21 +11,37 @@ import {
ThemeProvider ThemeProvider
} from './src/store'; } from './src/store';
import WindowComponent from './src/components/modalwindows/modalWindow.tsx'; import WindowComponent from './src/components/modalwindows/modalWindow';
import Router from './src/router.tsx'; import Router from './src/router';
import { I18nProvider } from "./src/utils/i18n/provider";
import { useLanguage } from "./src/hooks/useSettings";
const AppWithProviders = () => {
const currentLanguage = useLanguage();
return (
<I18nProvider initialLanguage={currentLanguage}>
<ThemeProvider>
<HashRouter>
<ModalProvider>
<MainComponent />
</ModalProvider>
</HashRouter>
</ThemeProvider>
</I18nProvider>
);
};
const MainComponent = () => { const MainComponent = () => {
const checkAdmin = useCheckAdmin();
const checkAdmin = useCheckAdmin() const { ipAddress } = useIpStore();
const {ipAddress} = useIpStore()
useEffect(() => { useEffect(() => {
const checkAdminStatus = async () => { const checkAdminStatus = async () => {
await checkAdmin() await checkAdmin();
} };
checkAdminStatus() checkAdminStatus();
}, [ipAddress]) }, [ipAddress]);
const { isOpen, modalType, modalData } = useModalContext(); const { isOpen, modalType, modalData } = useModalContext();
return ( return (
@ -40,13 +56,6 @@ const container = document.getElementById('root');
if (!container) { if (!container) {
throw new Error('Root element with id "root" not found'); throw new Error('Root element with id "root" not found');
} }
const root = createRoot(container); const root = createRoot(container);
root.render( root.render(<AppWithProviders />);
<ThemeProvider>
<HashRouter>
<ModalProvider>
<MainComponent />
</ModalProvider>
</HashRouter>
</ThemeProvider>
);

View file

@ -28,7 +28,7 @@
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-basic-ssl": "^2.0.0", "@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
"electron": "^34.0.2", "electron": "^39.2.3",
"electron-packager": "^17.1.2", "electron-packager": "^17.1.2",
"esbuild-wasm": "^0.17.12", "esbuild-wasm": "^0.17.12",
"typescript": "^4.9.5", "typescript": "^4.9.5",
@ -1787,13 +1787,13 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.17", "version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
@ -1991,13 +1991,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.4", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@ -2046,9 +2046,9 @@
"optional": true "optional": true
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2138,6 +2138,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/camelize": { "node_modules/camelize": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
@ -2536,16 +2549,30 @@
"minimatch": "^3.0.4" "minimatch": "^3.0.4"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron": { "node_modules/electron": {
"version": "34.0.2", "version": "39.2.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-34.0.2.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz",
"integrity": "sha512-u3F+DSUlg9NaGS+9qnYmSRN8VjAnc3LJDDk1ye1uISJnh4gjG76y3681qLowsPMx4obvCP2eBINnmbLo0yT5WA==", "integrity": "sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^20.9.0", "@types/node": "^22.7.7",
"extract-zip": "^2.0.1" "extract-zip": "^2.0.1"
}, },
"bin": { "bin": {
@ -2685,9 +2712,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -2696,9 +2721,34 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -2949,13 +2999,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.1", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@ -3003,7 +3055,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -3072,6 +3123,30 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-package-info": { "node_modules/get-package-info": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz",
@ -3105,6 +3180,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@ -3208,9 +3296,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -3265,11 +3351,37 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -3568,6 +3680,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -4580,9 +4701,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.19.8", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -4648,9 +4769,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.4", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View file

@ -30,7 +30,7 @@
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-basic-ssl": "^2.0.0", "@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
"electron": "^34.0.2", "electron": "^39.2.3",
"electron-packager": "^17.1.2", "electron-packager": "^17.1.2",
"esbuild-wasm": "^0.17.12", "esbuild-wasm": "^0.17.12",
"typescript": "^4.9.5", "typescript": "^4.9.5",

View file

@ -1,19 +1,11 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => { send: (channel, data) => ipcRenderer.send(channel, data),
ipcRenderer.send(channel, data); on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
}, quitApp: () => ipcRenderer.send('quit-app'),
on: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
quitApp: () => {
ipcRenderer.send('quit-app');
},
onThemeChange: (callback) => { showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
ipcRenderer.on('theme-update', (event, theme) => { writeFile: (path, data) => ipcRenderer.invoke('write-file', { path, data }),
callback(theme); onThemeChange: (callback) => ipcRenderer.on('theme-update', (event, theme) => callback(theme))
});
}
}); });

View file

@ -0,0 +1,186 @@
{
"userScanner-title": "Subnet Node Scanning",
"userScanner-visible": "Display Method",
"userScanner-loading": "Scanning nodes",
"userScanner-refresh": "Refresh List",
"userScanner-scan": "Scan Nodes",
"userScanner-notFound": "Nodes not found",
"tableScanner-online": "Online",
"tableScanner-name": "Name",
"tableScanner-vendor": "Vendor",
"tableScanner-ip": "IP Address",
"tableScanner-ports": "Open Ports",
"tableScanner-activity": "Last Activity",
"tableScanner-note": "Note",
"tableScanner-action": "Action",
"tableScanner-unknown": "Unknown",
"tableScanner-noData": "No data to display",
"listUsers-activeNodes": "Active Nodes",
"listUsers-inactiveNodes": "Inactive Nodes",
"ipInput-placeholder": "Enter IP",
"ipInput-error": "Enter a valid IP address",
"userScanner-visible_table": "Table",
"userScanner-visible_list": "List",
"wifiScanner-title": "Wi-Fi Access Point Scanning",
"wifiScanner-scanning": "Scanning access points",
"wifiScanner-scanError": "Network scanning error",
"wifiScanner-refresh": "Refresh List",
"wifiScanner-scan": "Scan Access Points",
"wifiModal-bssid": "BSSID",
"wifiModal-vendor": "Vendor",
"wifiModal-signal": "Signal",
"wifiModal-channel": "Channel",
"wifiModal-encryption": "Encryption",
"wifiModal-frequency": "Frequency",
"wifiModal-hidden": "Hidden Network",
"wifiModal-note": "Note",
"wifiModal-connect": "Connect",
"wifiModal-disconnect": "Disconnect",
"wifiModal-unknown": "Unknown",
"wifiModal-none": "None",
"common-yes": "Yes",
"common-no": "No",
"addNode": "Add Node",
"enterNodeAddress": "Enter node address...",
"connectionLost": "Connection Lost",
"serverDisconnected": "Server connection interrupted",
"checkAddressAndRetry": "Please check the address availability and try again",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"retry": "Retry",
"connect": "Connect",
"submit": "Submit",
"common-back": "Back",
"error": "Error",
"networkConnection": "Network Connection",
"enterPassword": "Enter password...",
"connectingAttempt": "Attempting to connect to the network",
"connectionError": "Network connection error",
"connectionErrorLog": "Connection error: ",
"customizeTable-title": "Subnet Node Table Customization",
"wifiDisconnect-title": "Wi-Fi Network Disconnection",
"wifiDisconnect-confirm": "Confirm Disconnection",
"wifiDisconnect-message1": "You are about to disconnect from the network - ",
"wifiDisconnect-message2": " All active connections will be interrupted.",
"wifiDisconnect-disconnect": "Disconnect",
"wifiDisconnect-progress": "Disconnecting from the network",
"ipChange-title": "IP Address Change",
"ipChange-checkingIp": "Checking IP address validity",
"ipChange-generalInfo": "General Information",
"ipChange-interface": "Interface",
"ipChange-ipInfo": "IP Information",
"ipChange-currentIp": "Current IP",
"ipChange-currentGateway": "Current Gateway",
"ipChange-currentDns": "Current DNS",
"ipChange-newIp": "New IP",
"ipChange-newGateway": "New Gateway",
"ipChange-newDns": "New DNS",
"ipChange-checkError": "An error occurred while checking validity",
"ipChange-adminRequired": "To change network settings, you must log in as an administrator",
"note-edit": "Edit Note",
"note-add": "Add Note",
"note-enter": "Enter note...",
"auth-successLogin": "Successful Login",
"auth-successLogout": "Successful Logout",
"auth-getAccess": "Obtaining Access Rights",
"auth-enableAdmin": "Enable Administrative Access",
"auth-enterAdminPass": "Enter administrator password...",
"auth-wrongPass": "Incorrect operator password",
"auth-switchToLimited": "Switch to Limited Access",
"auth-limitedModeDesc": "In this mode, administrator privileges are restricted.",
"auth-restrictAccess": "Restrict Access",
"auth-closeHint": "This window can be closed, or it will disappear on its own",
"operatorConfig-title": "Node Operator Configuration",
"operatorConfig-instruction": "Before starting, you need to create an application operator password",
"operatorConfig-passwordPlaceholder": "Enter password...",
"operatorConfig-repeatPasswordPlaceholder": "Repeat password...",
"operatorConfig-passwordMismatch": "Passwords do not match!",
"operatorConfig-submit": "Submit",
"about-logoAlt": "Gossys Logo",
"about-title": "About the Application",
"about-description": "GoNets is a minimally viable cross-platform product (MVP) for demonstrating our approach to managing PC settings in a corporate environment.",
"about-featuresTitle": "Features",
"about-feature1": "Visualization of active network resources",
"about-feature2": "Configuration of local and remote network resource parameters",
"about-feature3": "Management of resource interaction with the GoNets Client",
"about-disclaimer": "The software is for demonstration and home use only.",
"about-goToPro": "Go to GoNets-PRO",
"about-homepage": "Homepage",
"about-documentation": "Documentation",
"about-license": "License",
"about-copyright": "Developed and maintained by Hybrid Office Systems LLC, 2024-2025. All rights reserved.",
"main-logoAlt": "Application Logo",
"main-networkSettings": "Network Settings",
"main-about": "About the Program",
"nodes-availableNodes": "Available Nodes",
"nodes-rootAddress": "Root Address",
"nodes-name": "Name",
"nodes-loading": "Loading",
"nodes-additionalNodes": "Additional Nodes",
"nodes-node": "Node",
"nodes-address": "Address",
"nodes-connectionError": "Failed to connect to the server at the address ",
"settings-nodesSection": "Subnet Nodes",
"settings-intervalScanning": "Interval Scanning",
"settings-scanInterval": "Node Scan Interval (min)",
"settings-displayNames": "Display Names When Missing",
"settings-ipAddress": "IP Address",
"settings-unknown": "Unknown",
"settings-rowsPerPage": "Rows Per Table",
"settings-displaySettings": "Nodes Display",
"settings-configure": "Configure",
"settings-clearSessionData": "Clear Session Data",
"settings-clear": "Clear",
"settings-clearFailed": "Data was not deleted: ",
"settings-exportData": "Export Nodes Data",
"settings-export": "Export",
"settings-saveNodesTitle": "Save Nodes Data",
"settings-exportSuccess": "Data exported successfully!",
"settings-exportError": "Export error: ",
"settings-status": "Online",
"settings-name": "Name",
"settings-vendor": "Manufacturer",
"settings-openPorts": "Open Ports",
"settings-lastActivity": "Last Activity",
"settings-note": "Note",
"settings-action": "Action",
"settings-title": "Settings",
"settings-resetSettings": "Reset Settings",
"settings-resetConfirm": "Are you sure you want to reset all settings to default values?",
"settings-cancel": "Cancel",
"settings-save": "Save",
"settings-fixErrors": "Fix errors before saving",
"settings-fillRequired": "Fill in all required fields",
"settings-dataNotDeleted": "Data was not deleted: ",
"settings-saveNetworksTitle": "Save Networks Data",
"settings-fieldRequired": "Field cannot be empty",
"settings-enterNumber": "Please enter a number",
"settings-valueTooSmall": "Value cannot be less than ",
"settings-valueTooLarge": "Value cannot be greater than ",
"settings-sections-subnetNodes": "Subnet Nodes",
"settings-sections-accessPoints": "Access Points",
"settings-sections-appearance": "Appearance",
"settings-exportNetworkData": "Export Access Points Data",
"settings-colorTheme": "Color Theme",
"settings-light": "Light",
"settings-dark": "Dark",
"settings-system": "As in System",
"settings-interfaceLanguage": "Interface Language",
"settings-russian": "Russian",
"settings-english": "English"
}

View file

@ -0,0 +1,192 @@
{
"userScanner-title": "Сканирование узлов подсети",
"userScanner-visible": "Способ отображения",
"userScanner-loading": "Cканирование узлов",
"userScanner-refresh": "Обновить список",
"userScanner-scan": "Сканировать узлы",
"userScanner-notFound": "Узлы не найдены",
"tableScanner-online": "Онлайн",
"tableScanner-name": "Имя",
"tableScanner-vendor": "Производитель",
"tableScanner-ip": "IP адрес",
"tableScanner-ports": "Открытые порты",
"tableScanner-activity": "Последняя активность",
"tableScanner-note": "Заметка",
"tableScanner-action": "Действие",
"tableScanner-unknown": "Неизвестно",
"tableScanner-noData": "Нет данных для отображения",
"listUsers-activeNodes": "Активные узлы",
"listUsers-inactiveNodes": "Неактивные узлы",
"ipInput-placeholder": "Введите IP",
"ipInput-error": "Введите корректный IP-адрес",
"userScanner-visible_table": "Таблица",
"userScanner-visible_list": "Список",
"wifiScanner-title": "Сканирование точек доступа Wi-Fi",
"wifiScanner-scanning": "Сканирование точек доступа",
"wifiScanner-scanError": "Ошибка сканирования сети",
"wifiScanner-refresh": "Обновить список",
"wifiScanner-scan": "Сканировать точки доступа",
"wifiModal-bssid": "BSSID",
"wifiModal-vendor": "Производитель",
"wifiModal-signal": "Сигнал",
"wifiModal-channel": "Канал",
"wifiModal-encryption": "Шифрование",
"wifiModal-frequency": "Частота",
"wifiModal-hidden": "Скрытая сеть",
"wifiModal-note": "Заметка",
"wifiModal-connect": "Подключиться",
"wifiModal-disconnect": "Отключиться",
"wifiModal-unknown": "Неизвестно",
"wifiModal-none": "Отсутствует",
"common-yes": "Да",
"common-no": "Нет",
"addNode": "Добавление узла",
"enterNodeAddress": "Введите адрес узла...",
"connectionLost": "Соединение прервано",
"serverDisconnected": "Произошёл разрыв с сервером",
"checkAddressAndRetry": "Пожалуйста, проверьте доступ к адресу и попробуйте снова",
"cancel": "Отменить",
"close": "Закрыть",
"save": "Сохранить",
"retry": "Повторить",
"connect": "Подключиться",
"common-back": "Назад",
"submit": "Подтвердить",
"error": "Ошибка",
"networkConnection": "Подключение к сети",
"enterPassword": "Введите пароль...",
"connectingAttempt": "Попытка подключения к сети",
"connectionError": "Ошибка подключения к сети",
"connectionErrorLog": "Ошибка подключения: ",
"customizeTable-title": "Кастомизация таблицы узлов подсети",
"wifiDisconnect-title": "Отключение от Wi-Fi сети",
"wifiDisconnect-confirm": "Подтверждение отключения",
"wifiDisconnect-message1": "Вы собираетесь отключиться от сети - ",
"wifiDisconnect-message2": " Все активные подключения будут прерваны.",
"wifiDisconnect-disconnect": "Отключиться",
"wifiDisconnect-progress": "Отключение от сети",
"ipChange-title": "Изменение IP-Адреса",
"ipChange-checkingIp": "Идет проверка корректности IP адреса",
"ipChange-generalInfo": "Общие сведения",
"ipChange-interface": "Интерфейс",
"ipChange-ipInfo": "Сведения об IP",
"ipChange-currentIp": "Текущий ip",
"ipChange-currentGateway": "Текущий шлюз",
"ipChange-currentDns": "Текущий DNS",
"ipChange-newIp": "Новый ip",
"ipChange-newGateway": "Новый шлюз",
"ipChange-newDns": "Новый DNS",
"ipChange-checkError": "Произошла ошибка проверки корректности",
"ipChange-adminRequired": "Для изменения сетевых настроек необходимо авторизироваться под администратором",
"note-edit": "Редактирование заметки",
"note-add": "Добавление заметки",
"note-enter": "Введите заметку...",
"auth-successLogin": "Успешный вход",
"auth-successLogout": "Успешный выход",
"auth-getAccess": "Получение прав доступа",
"auth-enableAdmin": "Включить административный доступ",
"auth-enterAdminPass": "Введите пароль администратора...",
"auth-wrongPass": "Неверный пароль оператора",
"auth-switchToLimited": "Переключиться на ограниченный доступ",
"auth-limitedModeDesc": "В данном режиме доступа ограничиваются полномочия администратора.",
"auth-restrictAccess": "Ограничить доступ",
"auth-closeHint": "Это окно можно закрыть, либо оно пропадёт само",
"operatorConfig-title": "Настройка оператора узла",
"operatorConfig-instruction": "Перед началом работы необходимо создать пароль оператора приложения",
"operatorConfig-passwordPlaceholder": "Введите пароль...",
"operatorConfig-repeatPasswordPlaceholder": "Повторите пароль...",
"operatorConfig-passwordMismatch": "Пароли не совпадают!",
"operatorConfig-submit": "Отправить",
"about-logoAlt": "Логотип Gossys",
"about-title": "О приложении",
"about-description": "ГоNets - минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.",
"about-featuresTitle": "Возможности",
"about-feature1": "Визуализация активных сетевых ресурсов",
"about-feature2": "Настройка параметров локальных и удалённых сетевых ресурсов",
"about-feature3": "Управление взаимодействием ресурсов с Клиентом ГoNets",
"about-disclaimer": "Программное обеспечение исключительно для ознакомления и домашнего использования.",
"about-goToPro": "Перейти на ГоNets-PRO",
"about-homepage": "Домашняя страница",
"about-documentation": "Документация",
"about-license": "Лицензия",
"about-copyright": "Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.",
"main-logoAlt": "Логотип приложения",
"main-networkSettings": "Настройки сети",
"main-about": "О программе",
"nodes-availableNodes": "Доступные узлы",
"nodes-rootAddress": "Корневой адрес",
"nodes-name": "Имя",
"nodes-loading": "Загрузка",
"nodes-additionalNodes": "Дополнительные узлы",
"nodes-node": "Узел",
"nodes-address": "Адрес",
"nodes-connectionError": "Не удалось подключиться к серверу по адресу ",
"settings-nodesSection": "Узлы подсети",
"settings-intervalScanning": "Интервальное сканирование",
"settings-scanInterval": "Интервал сканирования узлов (мин)",
"settings-displayNames": "Отображаемые имена при отсутствии",
"settings-ipAddress": "IP адрес",
"settings-unknown": "Неизвестно",
"settings-rowsPerPage": "Количество строк в таблице",
"settings-displaySettings": "Отображение узлов",
"settings-configure": "Настроить",
"settings-clearSessionData": "Очистить данные сессии",
"settings-clear": "Очистить",
"settings-clearFailed": "Данные не были удалены: ",
"settings-exportData": "Экспорт данных узлов",
"settings-export": "Экспортировать",
"settings-saveNodesTitle": "Сохранить данные узлов",
"settings-exportSuccess": "Данные успешно экспортированы!",
"settings-exportError": "Ошибка экспорта: ",
"settings-status": "Онлайн",
"settings-name": "Имя",
"settings-vendor": "Производитель",
"settings-openPorts": "Открытые порты",
"settings-lastActivity": "Последняя активность",
"settings-note": "Заметка",
"settings-action": "Действие",
"settings-title": "Настройки",
"settings-resetSettings": "Сбросить настройки",
"settings-resetConfirm": "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?",
"settings-cancel": "Отменить",
"settings-save": "Сохранить",
"settings-fixErrors": "Исправьте ошибки перед сохранением",
"settings-fillRequired": "Заполните все обязательные поля",
"settings-dataNotDeleted": "Данные не были удалены: ",
"settings-saveNetworksTitle": "Сохранить данные сетей",
"settings-fieldRequired": "Поле не может быть пустым",
"settings-enterNumber": "Пожалуйста, введите число",
"settings-valueTooSmall": "Значение не может быть меньше ",
"settings-valueTooLarge": "Значение не может быть больше ",
"settings-sections-subnetNodes": "Узлы подсети",
"settings-sections-accessPoints": "Точки доступа",
"settings-sections-appearance": "Внешний вид",
"settings-exportNetworkData": "Экспорт данных точек доступа",
"settings-colorTheme": "Цветовая тема",
"settings-light": "Светлая",
"settings-dark": "Тёмная",
"settings-system": "Как в системе",
"settings-interfaceLanguage": "Язык интерфейса",
"settings-russian": "Русский",
"settings-english": "Английский"
}

View file

@ -1,13 +1,13 @@
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
const axiosWithoutCredentials = axios.create({ const axiosWithoutCredentials = axios.create({
baseURL: `https://localhost:8001`, baseURL: `http://localhost:8001`,
}) })
export class NetworkService { export class NetworkService {
static createAxiosInstance(ipAddress: string) { static createAxiosInstance(ipAddress: string) {
return axios.create({ return axios.create({
baseURL: `https://${ipAddress}:8001`, baseURL: `http://${ipAddress}:8001`,
}); });
} }
@ -83,10 +83,12 @@ export class NetworkService {
} }
} }
static async scanUser(ipAddress: string): Promise<AxiosResponse> { static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
try { try {
const axiosInstance = this.createAxiosInstance(ipAddress); const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices'); return await axiosInstance.post('/subnet/devices',{
subnet: ipScanning,
});
} catch (error: any) { } catch (error: any) {
return error.response; return error.response;
} }
@ -157,6 +159,8 @@ export class NetworkService {
return error.response; return error.response;
} }
} }
// Settings
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> { static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
try { try {
const axiosInstance = this.createAxiosInstance(ipAddress); const axiosInstance = this.createAxiosInstance(ipAddress);
@ -173,4 +177,20 @@ export class NetworkService {
return error.response; return error.response;
} }
} }
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_devices`);
} catch (error: any) {
return error.response;
}
}
static async exportNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_wifi`);
} catch (error: any) {
return error.response;
}
}
} }

View file

@ -8,6 +8,7 @@ import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader"; import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components"; import {useTheme} from "styled-components";
import {useIpStore} from "../../../store"; import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props { interface Props {
address: string; address: string;
@ -22,50 +23,52 @@ export const ConnectNetwork = ({address, onClose}: Props) => {
const {ipAddress} = useIpStore(); const {ipAddress} = useIpStore();
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleSave = async () => { const handleSave = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError('') setError('');
const res = await NetworkService.connectNetwork(ipAddress, password, address); const res = await NetworkService.connectNetwork(ipAddress, password, address);
if (res.status === 200) { if (res.status === 200) {
onClose() onClose();
} else { } else {
setError(res.data.detail) setError(res.data.detail);
} }
} catch (error) { } catch (error) {
console.error('Error connection:', error); console.error(t('connectionErrorLog'), error);
setError('Ошибка подключения к сети') setError(t('connectionError'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<ModalContainer style={{height: '230px'}}> <ModalContainer style={{height: '230px'}}>
<ModalTitle> <ModalTitle>
<h3>Подключение к сети</h3> <h3>{t('networkConnection')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}> <ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<Input <Input
type="password" type="password"
placeholderText='Введите пароль...' placeholderText={t('enterPassword')}
value={password} value={password}
id='deviceNote' id='deviceNote'
onChange={(value) => setPassword(value)} onChange={(value) => setPassword(value)}
/> />
{error && (
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}> <p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
{error} {error}
</p> </p>
)}
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title="Отмена" title={t('cancel')}
onClick={onClose} onClick={onClose}
/> />
<MainButton <MainButton
@ -73,12 +76,12 @@ export const ConnectNetwork = ({address, onClose}: Props) => {
size="small" size="small"
color={theme.successColor} color={theme.successColor}
hoverColor={theme.successColor} hoverColor={theme.successColor}
title='Подключиться' title={t('connect')}
onClick={handleSave} onClick={handleSave}
/> />
</ButtonContainer> </ButtonContainer>
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>} {isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
</ModalContainer> </ModalContainer>
); );
}; };

View file

@ -1,4 +1,3 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style"; import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton"; import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input"; import Input from "../../UI/inputs/input";
@ -6,6 +5,7 @@ import { useState } from "react";
import {useIpStore} from "../../../store"; import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request"; import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore"; import useNetworkStore from "../../../store/useDeviceStore";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props { interface Props {
address: string; address: string;
@ -20,6 +20,7 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || ''); const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore(); const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore(); const { devices, setDevices } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => { const handleSave = async () => {
try { try {
@ -50,13 +51,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
return ( return (
<ModalContainer style={{ height: '250px' }}> <ModalContainer style={{ height: '250px' }}>
<ModalTitle> <ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3> <h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input <Input
type="text" type="text"
placeholderText='Введите заметку...' placeholderText={t('note-enter')}
value={note} value={note}
id='deviceNote' id='deviceNote'
onChange={(value) => setNote(value)} onChange={(value) => setNote(value)}
@ -67,13 +68,13 @@ export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title="Отмена" title={t('cancel')}
onClick={onClose} onClick={onClose}
/> />
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Сохранить' title={t('save')}
onClick={handleSave} onClick={handleSave}
/> />
</ButtonContainer> </ButtonContainer>

View file

@ -6,6 +6,7 @@ import { useState } from "react";
import {NetworkService} from "../../../api/request"; import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore"; import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store"; import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props { interface Props {
address: string; address: string;
@ -20,6 +21,7 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || ''); const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore(); const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore(); const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => { const handleSave = async () => {
try { try {
@ -50,13 +52,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
return ( return (
<ModalContainer style={{ height: '220px' }}> <ModalContainer style={{ height: '220px' }}>
<ModalTitle> <ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3> <h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> <ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input <Input
type="text" type="text"
placeholderText='Введите заметку...' placeholderText={t('note-enter')}
value={note} value={note}
id='deviceNote' id='deviceNote'
onChange={(value) => setNote(value)} onChange={(value) => setNote(value)}
@ -67,13 +69,13 @@ export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title="Отмена" title={t('cancel')}
onClick={onClose} onClick={onClose}
/> />
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Сохранить' title={t('save')}
onClick={handleSave} onClick={handleSave}
/> />
</ButtonContainer> </ButtonContainer>

View file

@ -3,6 +3,7 @@ import { MainButton } from "../../UI/buttons/mainButton";
import { useState } from "react"; import { useState } from "react";
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style" import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { validateIPAddress } from "../../../utils/nodeController/validator"; import { validateIPAddress } from "../../../utils/nodeController/validator";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props { interface Props {
onSave?: (data: string) => void; onSave?: (data: string) => void;
@ -12,6 +13,7 @@ interface Props {
export const AddDevice = ({ onSave, onClose }: Props) => { export const AddDevice = ({ onSave, onClose }: Props) => {
const [ipAddress, setIpAddress] = useState('') const [ipAddress, setIpAddress] = useState('')
const [error, setError] = useState(false) const [error, setError] = useState(false)
const {t} = useTranslation('common');
const handleSubmit = () => { const handleSubmit = () => {
if (!validateIPAddress(ipAddress)) { if (!validateIPAddress(ipAddress)) {
@ -28,31 +30,39 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
return ( return (
<ModalContainer style={{ height: '230px' }}> <ModalContainer style={{ height: '230px' }}>
{/* <form onSubmit={handleSubmit}> */}
<ModalTitle> <ModalTitle>
<h3>Добавление узла</h3> <h3>{t('addNode')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}> <ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<Input <Input
type="text" type="text"
placeholderText='Введите адрес узла...' placeholderText={t('enterNodeAddress')}
id="newIp" id="newIp"
value={ipAddress} value={ipAddress}
onChange={(value) => setIpAddress(value)} onChange={(value) => setIpAddress(value)}
/> />
{error && ( {error && (
<p style={{ color: 'red', marginTop: '5px' }}> <p style={{ color: 'red', marginTop: '5px' }}>
Некорректный адрес узла {t('invalidAddress')}
</p> </p>
)} )}
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton type="button" size="small" title="Отмена" onClick={onClose} /> <MainButton
<MainButton type="submit" size="small" title="Сохранить" onClick={handleSubmit} /> type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="submit"
size="small"
title={t('save')}
onClick={handleSubmit}
/>
</ButtonContainer> </ButtonContainer>
{/* </form> */}
</ModalContainer> </ModalContainer>
) )
} }

View file

@ -2,6 +2,7 @@ import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer }
import { useModalContext } from "../../../store" import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store" import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton" import { MainButton } from "../../UI/buttons/mainButton"
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={ type Props={
message: string message: string
@ -10,6 +11,7 @@ type Props={
export const ConnectionError = ({message}: Props)=>{ export const ConnectionError = ({message}: Props)=>{
const {closeModal} = useModalContext() const {closeModal} = useModalContext()
const { setIpAddress } = useIpStore(); const { setIpAddress } = useIpStore();
const {t} = useTranslation('common');
const close = ()=>{ const close = ()=>{
setIpAddress('localhost') setIpAddress('localhost')
@ -19,19 +21,27 @@ export const ConnectionError = ({message}: Props)=>{
return ( return (
<ModalContainer> <ModalContainer>
<ModalTitle> <ModalTitle>
<h3>Соединение прервано</h3> <h3>{t('connectionLost')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent> <ModalContent>
<ModalText> <ModalText>
<h3>Произошёл разрыв с сервером</h3> <h3>{t('serverDisconnected')}</h3>
<p>Пожалуйста, проверьте доступ к адресу и попробуйте снова.</p> <p>{t('checkAddressAndRetry')}</p>
</ModalText> </ModalText>
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton title="Закрыть" onClick={close} size="small"/> <MainButton
<MainButton title="Повторить" onClick={closeModal} size="small"/> title={t('close')}
onClick={close}
size="small"
/>
<MainButton
title={t('retry')}
onClick={closeModal}
size="small"
/>
</ButtonContainer> </ButtonContainer>
</ModalContainer> </ModalContainer>
) )

View file

@ -0,0 +1,143 @@
import React, {useEffect, useState} from 'react';
import {ButtonContainer, ModalContainer, ModalContent, ModalTitle} from "../style";
import {MainButton} from "../../UI/buttons/mainButton";
import {ModalMap} from "../windowsType";
import {useModalContext} from "../../../store";
import styled from "styled-components";
import { useSettingsStore } from '../../../store/settings/settingsStore';
import {useTranslation} from "../../../utils/i18n/hooks";
const StyledTableList = styled.ul`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 0 40px;
list-style-type: none;
`
const StyledTableItem = styled.li`
width: 160px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
background-color: ${props => props.theme.accent};
color: ${props => props.theme.primary};
border: 1px solid ${props => props.theme.primary};
&:first-child {
border-radius: 16px 0 0 16px;
}
&:last-child {
border-radius: 0 16px 16px 0;
}
&.off{
background-color: ${props => props.theme.offlineColor};
}
`
const CustomizeNodes = () => {
const { openModal } = useModalContext();
const pendingChanges = useSettingsStore(state => state.pendingChanges);
const updateSetting = useSettingsStore(state => state.updateSetting);
const getSetting = useSettingsStore(state => state.getSetting);
const initializeSettings = useSettingsStore(state => state.initializeSettings);
const {t} = useTranslation('common');
const [dragIndex, setDragIndex] = useState<number | null>(null);
// Получаем текущие значения из хранилища
const initialColumns = getSetting('table-columns-order').currentValue as string[];
const disabledColumns = getSetting('table-columns-disabled').currentValue as number[];
const items = pendingChanges['table-columns-order'] ?? initialColumns;
const disabledItems = pendingChanges['table-columns-disabled'] ?? disabledColumns;
// Drag & Drop
const handleDragStart = (index: number) => {
setDragIndex(index);
};
const handleDragOver = (index: number) => {
if (dragIndex === null || index === dragIndex) return;
const newItems = [...items];
const [movedItem] = newItems.splice(dragIndex, 1);
newItems.splice(index, 0, movedItem);
updateSetting('table-columns-order', newItems);
setDragIndex(index);
};
const handleDrop = () => {
setDragIndex(null);
};
// Переключение состояния
const handleSwitch = (index: number) => {
const newDisabledItems = disabledItems.includes(index)
? disabledItems.filter((i: number) => i !== index)
: [...disabledItems, index];
updateSetting('table-columns-disabled', newDisabledItems);
};
// Закрытие с сохранением
const handleSave = () => {
openModal(ModalMap['10']);
};
useEffect(() => {
initializeSettings();
}, [initializeSettings]);
const getItemText = (item: string) => {
if (item === 'Онлайн') return t('settings-status');
if (item === 'Имя') return t('settings-name');
if (item === 'Производитель') return t('settings-vendor');
if (item === 'IP адрес') return t('settings-ipAddress');
if (item === 'Открытые порты') return t('settings-openPorts');
if (item === 'Последняя активность') return t('settings-lastActivity');
if (item === 'Заметка') return t('settings-note');
if (item === 'Действие') return t('settings-action');
return item;
};
return (
<ModalContainer style={{ height: '35vh', width: '80vw'}}>
<ModalTitle>
<h3>{t('customizeTable-title')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', height: "100%", gap: '24px', padding: 0 }}>
<StyledTableList>
{items.map((item:string, index:number) => (
<StyledTableItem
key={index}
className={disabledItems.includes(index) ? 'off' : ''}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => {
e.preventDefault();
handleDragOver(index);
}}
onDragEnd={handleDrop}
onClick={() => handleSwitch(index)}
>
{getItemText(item)}
</StyledTableItem>
))}
</StyledTableList>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('common-back')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};
export default CustomizeNodes;

View file

@ -6,6 +6,7 @@ import {useState} from "react";
import {NetworkService} from "../../../api/request"; import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader"; import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components"; import {useTheme} from "styled-components";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={ type Props={
address: string, address: string,
@ -16,6 +17,7 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore(); const { ipAddress } = useIpStore();
const theme = useTheme() const theme = useTheme()
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleDisconnect = async () => { const handleDisconnect = async () => {
try { try {
@ -34,21 +36,32 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
return( return(
<ModalContainer> <ModalContainer>
<ModalTitle> <ModalTitle>
<h3>Отключение от Wi-Fi сети</h3> <h3>{t('wifiDisconnect-title')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent> <ModalContent>
<ModalText> <ModalText>
<h3>Подтверждение отключения</h3> <h3>{t('wifiDisconnect-confirm')}</h3>
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p> <p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText> </ModalText>
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton title="Отменить" onClick={onClose} size="small"/> <MainButton
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/> title={t('cancel')}
onClick={onClose}
size="small"
/>
<MainButton
title={t('wifiDisconnect-disconnect')}
color={theme.errorColor}
hoverColor={theme.errorColor}
onClick={handleDisconnect}
size="small"
/>
</ButtonContainer> </ButtonContainer>
{isLoading && <RouterLoader loaderText='Отключение от сети'/>} {isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
</ModalContainer> </ModalContainer>
) )
} }

View file

@ -8,6 +8,7 @@ import { useIpStore } from "../../../store";
import { MainButton } from "../../UI/buttons/mainButton"; import { MainButton } from "../../UI/buttons/mainButton";
import { useAdminStore } from "../../../store/adminStore"; import { useAdminStore } from "../../../store/adminStore";
import { useDevicesStore } from "../../../store"; import { useDevicesStore } from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props = { type Props = {
ipData: changeIp ipData: changeIp
@ -52,6 +53,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
const { ipAddress, setIpAddress } = useIpStore(); const { ipAddress, setIpAddress } = useIpStore();
const {isAdmin} = useAdminStore() const {isAdmin} = useAdminStore()
const { devices, addDevice, deleteDevice } = useDevicesStore(); const { devices, addDevice, deleteDevice } = useDevicesStore();
const {t} = useTranslation('common');
useEffect(() => { useEffect(() => {
if (ipData && isAdmin) { if (ipData && isAdmin) {
@ -146,40 +148,48 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
return ( return (
<> <>
{loader && isAdmin ? ( {loader && isAdmin ? (
<Loader loaderText="Идет проверка корректности IP адреса" /> <Loader loaderText={t('ipChange-checkingIp')} />
) : ( ) : (
<ModalContainer {...(checkStatus ? { height: "unset" } : {})}> <ModalContainer {...(checkStatus ? { height: "unset" } : {})}>
<ModalTitle> <ModalTitle>
<h3>Изменение IP-Адреса</h3> <h3>{t('ipChange-title')}</h3>
</ModalTitle> </ModalTitle>
{checkStatus ? ( {checkStatus ? (
<ModalContent> <ModalContent>
<Information> <Information>
<h3>Общие сведения</h3> <h3>{t('ipChange-generalInfo')}</h3>
<Info> <Info>
<p>Интерфейс: {ipData.device}</p> <p>{t('ipChange-interface')}: {ipData.device}</p>
<p>--{'>'}</p> <p>--{'>'}</p>
<p>{ipData.connection}</p> <p>{ipData.connection}</p>
</Info> </Info>
<h3>Сведения об IP</h3> <h3>{t('ipChange-ipInfo')}</h3>
<DataContainer> <DataContainer>
<CurrentData> <CurrentData>
<p>Текущий ip: {ipData.currentIp}</p> <p>{t('ipChange-currentIp')}: {ipData.currentIp}</p>
<p>Текущий шлюз: {ipData.currentGate}</p> <p>{t('ipChange-currentGateway')}: {ipData.currentGate}</p>
<p>Текущий DNS: {ipData.currentDns}</p> <p>{t('ipChange-currentDns')}: {ipData.currentDns}</p>
</CurrentData> </CurrentData>
<NewData> <NewData>
<p>Новый ip: {ipData.newIp}</p> <p>{t('ipChange-newIp')}: {ipData.newIp}</p>
<p>Новый шлюз: {ipData.newGate}</p> <p>{t('ipChange-newGateway')}: {ipData.newGate}</p>
<p>Новый DNS: {ipData.newDns}</p> <p>{t('ipChange-newDns')}: {ipData.newDns}</p>
</NewData> </NewData>
</DataContainer> </DataContainer>
</Information> </Information>
<ButtonContainer> <ButtonContainer>
<MainButton title="Отмена" size='small' onClick={() => cancelIp(currentIp)}/> <MainButton
<MainButton title="Сохранить" size='small' onClick={() => saveIp(newIP)}/> title={t('cancel')}
size='small'
onClick={() => cancelIp(currentIp)}
/>
<MainButton
title={t('save')}
size='small'
onClick={() => saveIp(newIP)}
/>
</ButtonContainer> </ButtonContainer>
</ModalContent> </ModalContent>
) : ( ) : (
@ -188,19 +198,21 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
<ModalText> <ModalText>
{isAdmin ? ( {isAdmin ? (
<> <>
<h3>Произошла ошибка проверки корректности</h3> <h3>{t('ipChange-checkError')}</h3>
<ErrorMessage>{message}</ErrorMessage> <ErrorMessage>{message}</ErrorMessage>
</> </>
):( ):(
<h3>Для изменения сетевых настроек необходимо авторизироваться под администратором</h3> <h3>{t('ipChange-adminRequired')}</h3>
)} )}
</ModalText> </ModalText>
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton title="Закрыть" size='small' onClick={smoothClose}/> <MainButton
title={t('close')}
size='small'
onClick={smoothClose}
/>
</ButtonContainer> </ButtonContainer>
</> </>
)} )}

View file

@ -9,6 +9,7 @@ import { useCheckAdmin } from "../../../utils/checkAdmin"
import { buttonNames } from "../../../const/buttons/names" import { buttonNames } from "../../../const/buttons/names"
import { IconComponent } from "../../../const/buttons/icons" import { IconComponent } from "../../../const/buttons/icons"
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@ -17,9 +18,10 @@ interface Props {
export const RightsConfirm = ({ onClose }: Props) => { export const RightsConfirm = ({ onClose }: Props) => {
const { ipAddress } = useIpStore(); const { ipAddress } = useIpStore();
const { isAdmin, setAdmin } = useAdminStore() const { isAdmin } = useAdminStore()
const checkAdmin = useCheckAdmin(); const checkAdmin = useCheckAdmin();
const [error, setError] = useState(false) const [error, setError] = useState(false)
const {t} = useTranslation('common');
const [success, setSuccess] = useState({ const [success, setSuccess] = useState({
message: '', message: '',
status: false status: false
@ -45,7 +47,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
setError(false) setError(false)
setSuccess(prevData => ({ setSuccess(prevData => ({
...prevData, ...prevData,
message: 'Успешный вход', message: t('auth-successLogin'),
status: true status: true
})); }));
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
@ -54,7 +56,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
setError(true) setError(true)
} }
} catch (err) { } catch (err) {
console.error('Error:', err); console.error(t('auth-error'), err);
} }
} }
@ -63,7 +65,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
await checkAdmin(); await checkAdmin();
setSuccess(prevData => ({ setSuccess(prevData => ({
...prevData, ...prevData,
message: 'Успешный выход', message: t('auth-successLogout'),
status: true status: true
})); }));
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
@ -76,15 +78,14 @@ export const RightsConfirm = ({ onClose }: Props) => {
!isAdmin ? ( !isAdmin ? (
<> <>
<ModalTitle> <ModalTitle>
<h3>Получение прав доступа</h3> <h3>{t('auth-getAccess')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}> <ModalContent style={{display: 'flex', flexDirection: 'column', gap: '10px'}}>
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>{t('auth-enableAdmin')}</h3>
<h3 style={{marginLeft: '5px', fontSize: '14px'}}>Включить административный доступ</h3>
<Input <Input
type="password" type="password"
placeholderText='Введите пароль администратора...' placeholderText={t('auth-enterAdminPass')}
id="password" id="password"
value={user.password} value={user.password}
onChange={(value) => handleChange('password', value)} onChange={(value) => handleChange('password', value)}
@ -92,44 +93,40 @@ export const RightsConfirm = ({ onClose }: Props) => {
{error && ( {error && (
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}> <p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
Неверный пароль оператора {t('auth-wrongPass')}
</p> </p>
)} )}
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton size="small" title="Отмена" onClick={onClose} /> <MainButton size="small" title={t('cancel')} onClick={onClose} />
<MainButton size="small" title="Отправить" onClick={() => login(user)} /> <MainButton size="small" title={t('submit')} onClick={() => login(user)} />
</ButtonContainer> </ButtonContainer>
</> </>
) : ( ) : (
<> <>
<ModalTitle> <ModalTitle>
<h3>Переключиться на ограниченный доступ</h3> <h3>{t('auth-switchToLimited')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent> <ModalContent>
<p>В данном режиме доступа ограничиваются полномочия администратора.</p> <p>{t('auth-limitedModeDesc')}</p>
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<MainButton size="small" title="Отмена" onClick={onClose} /> <MainButton size="small" title={t('cancel')} onClick={onClose} />
<MainButton size="small" title="Ограничить доступ" onClick={logout} /> <MainButton size="small" title={t('auth-restrictAccess')} onClick={logout} />
</ButtonContainer> </ButtonContainer>
</> </>
) )
):( ):(
<ModalContainer style={{display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '15px'}}> <ModalContainer style={{display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '15px'}}>
<IconComponent name={buttonNames[8]} width="50" height="50"/> <IconComponent name={buttonNames[8]} width="50" height="50"/>
<h3>{success.message}</h3> <h3>{success.message}</h3>
<p style={{fontSize: '12px'}}>Это окно можно закрыть, либо оно пропадёт само</p> <p style={{fontSize: '12px'}}>{t('auth-closeHint')}</p>
<MainButton size="small" title="Закрыть" onClick={onClose} style={{width: '30%'}}/> <MainButton size="small" title={t('close')} onClick={onClose} style={{width: '30%'}}/>
</ModalContainer> </ModalContainer>
)} )}
</ModalContainer> </ModalContainer>
) )
} }

View file

@ -8,6 +8,7 @@ import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork"; import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm"; import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
import {Settings} from "../settings/Settings"; import {Settings} from "../settings/Settings";
import CustomizeNodes from "./modalContainers/customizeNodes";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,
@ -80,6 +81,9 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
onClose={closeLoader} onClose={closeLoader}
/> />
)} )}
{type === ModalMap['11'] && (
<CustomizeNodes />
)}
</ModalWindow > </ModalWindow >
} }
</> </>

View file

@ -32,6 +32,7 @@ export const ModalTitle = styled.div`
padding: 20px; padding: 20px;
border-bottom: 1px solid #9D9D9D; border-bottom: 1px solid #9D9D9D;
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
h3{ h3{

View file

@ -10,4 +10,6 @@ export const ModalMap: ModalType = {
'7': 'noteWifi', '7': 'noteWifi',
'8': 'connectNetwork', '8': 'connectNetwork',
'9': 'disconnectConfirm', '9': 'disconnectConfirm',
"10": 'settings',
'11': 'customNodes',
} }

View file

@ -9,6 +9,7 @@ import { IconComponent } from "../const/buttons/icons"
import { buttonNames } from "../const/buttons/names" import { buttonNames } from "../const/buttons/names"
import ThemeButton from "./UI/buttons/toggletheme" import ThemeButton from "./UI/buttons/toggletheme"
import About from "./workSpace/modules/appModules/about" import About from "./workSpace/modules/appModules/about"
import {useTranslation} from "../utils/i18n/hooks";
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
@ -73,6 +74,7 @@ export const CreateUser = () => {
const [repeatPassword, setRepeatPassword] = useState(''); const [repeatPassword, setRepeatPassword] = useState('');
const [passwordsMatch, setPasswordsMatch] = useState(true); const [passwordsMatch, setPasswordsMatch] = useState(true);
const [about, setAbout] = useState(false) const [about, setAbout] = useState(false)
const {t} = useTranslation('common')
const [user, setUser] = useState({ const [user, setUser] = useState({
username: 'operator', username: 'operator',
@ -118,31 +120,27 @@ export const CreateUser = () => {
setAbout(true) setAbout(true)
} }
return ( return (
<> <>
{!about ? ( {!about ? (
<Wrapper> <Wrapper>
<Container onSubmit={handleSubmit}> <Container onSubmit={handleSubmit}>
<Title> <Title>
<IconComponent name={buttonNames[3]} width="52" height="52"></IconComponent> <IconComponent name={buttonNames[3]} width="52" height="52" />
<h3>Настройка оператора узла</h3> <h3>{t('operatorConfig-title')}</h3>
{ipAddress != 'localhost' && {ipAddress != 'localhost' &&
<p>{ipAddress}</p> <p>{ipAddress}</p>
} }
</Title> </Title>
<Content> <Content>
<div> <div>
<p>Перед началом работы необходимо создать пароль оператора приложения</p> <p>{t('operatorConfig-instruction')}</p>
</div> </div>
<div> <div>
<Input <Input
type="password" type="password"
placeholderText='Введите пароль...' placeholderText={t('operatorConfig-passwordPlaceholder')}
id="password" id="password"
value={user.password} value={user.password}
onChange={(value) => handleChange('password', value)} onChange={(value) => handleChange('password', value)}
@ -150,7 +148,7 @@ export const CreateUser = () => {
<Input <Input
type="password" type="password"
placeholderText='Повторите пароль...' placeholderText={t('operatorConfig-repeatPasswordPlaceholder')}
id="repeatPassword" id="repeatPassword"
value={repeatPassword} value={repeatPassword}
onChange={(value) => handleChange('repeatPassword', value)} onChange={(value) => handleChange('repeatPassword', value)}
@ -158,29 +156,38 @@ export const CreateUser = () => {
/> />
{!passwordsMatch && ( {!passwordsMatch && (
<p style={{ color: 'red', marginTop: '5px' }}> <p style={{ color: 'red', marginTop: '5px' }}>
Пароли не совпадают! {t('operatorConfig-passwordMismatch')}
</p> </p>
)} )}
</div> </div>
</Content> </Content>
<ButtonContainer> <ButtonContainer>
<MainButton style={{ width: '100%' }} title="Отправить" type="submit" /> <MainButton
style={{ width: '100%' }}
title={t('operatorConfig-submit')}
type="submit"
/>
</ButtonContainer> </ButtonContainer>
</Container> </Container>
<Menu> <Menu>
{ipAddress != 'localhost' && {ipAddress != 'localhost' &&
<MainButton title="Назад" onClick={back} style={{marginRight: '10px'}}/> <MainButton
title={t('common-back')}
onClick={back}
style={{marginRight: '10px'}}
/>
} }
<ThemeButton /> <ThemeButton />
<Button> <Button>
<IconContainer> <IconContainer>
<IconComponent name={buttonNames[4]} width="30" height="30" onClick={toAbout}/> <IconComponent
name={buttonNames[4]}
width="30"
height="30"
onClick={toAbout}
/>
</IconContainer> </IconContainer>
</Button> </Button>
</Menu> </Menu>
@ -189,6 +196,5 @@ export const CreateUser = () => {
<About from="reg" onBack={() => setAbout(false)} /> <About from="reg" onBack={() => setAbout(false)} />
)} )}
</> </>
);
)
} }

View file

@ -6,6 +6,9 @@ import {buttonNames} from "../../const/buttons/names";
import SettingSelect from "./SettingSelect"; import SettingSelect from "./SettingSelect";
import {MainButton} from "../UI/buttons/mainButton"; import {MainButton} from "../UI/buttons/mainButton";
import React from 'react'; import React from 'react';
import {useModalContext} from "../../store";
import {ModalMap} from "../modalwindows/windowsType";
import {useTranslation} from "../../utils/i18n/hooks";
const SettingBlock = styled.div` const SettingBlock = styled.div`
display: flex; display: flex;
@ -19,6 +22,7 @@ const SettingBlock = styled.div`
font-size: 16px; font-size: 16px;
outline: none; outline: none;
background-color: ${props => props.theme.primary}; background-color: ${props => props.theme.primary};
color: ${props => props.theme.textColor};
border: 1px solid ${props => props.theme.borderColor}; border: 1px solid ${props => props.theme.borderColor};
padding: 6px 8px; padding: 6px 8px;
border-radius: 4px; border-radius: 4px;
@ -73,7 +77,7 @@ const InputWrapper = styled.div`
gap: 6px; gap: 6px;
` `
const ErrorText = styled.div` const ErrorText = styled.div`
color: red; color: ${props => props.theme.errorColor};
font-size: 12px; font-size: 12px;
`; `;
@ -86,26 +90,65 @@ interface SettingItemProps {
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => { const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation('common');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { openModal } = useModalContext();
const handleButtonAction = () => {
if (setting.id === 'customize-nodes') {
openModal(ModalMap['11']);
} else if (setting.action) {
// Создаем обертку для действия с переводами
const actionWithTranslations = async () => {
try {
// Временно заменяем alert на версию с переводами
const originalAlert = window.alert;
window.alert = (message: string) => {
// Применяем переводы к сообщениям
let translatedMessage = message;
if (message.startsWith('settings-dataNotDeleted')) {
translatedMessage = t('settings-dataNotDeleted') + message.replace('settings-dataNotDeleted', '');
} else if (message === 'settings-exportSuccess') {
translatedMessage = t('settings-exportSuccess');
} else if (message.startsWith('settings-exportError')) {
translatedMessage = t('settings-exportError') + message.replace('settings-exportError', '');
}
originalAlert(translatedMessage);
};
if (setting.action) {
await setting.action();
}
// Восстанавливаем оригинальный alert
window.alert = originalAlert;
} catch (error) {
console.error('Action error:', error);
}
};
actionWithTranslations();
}
};
// Изменение числа
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value; const inputValue = e.target.value;
let newError: string | null = null; let newError: string | null = null;
if (inputValue.trim() === '') { if (inputValue.trim() === '') {
newError = 'Поле не может быть пустым'; newError = t('settings-fieldRequired');
onChange(null); onChange(null);
} }
else if (isNaN(Number(inputValue))) { else if (isNaN(Number(inputValue))) {
newError = 'Пожалуйста, введите число'; newError = t('settings-enterNumber');
} }
else { else {
const numValue = Number(inputValue); const numValue = Number(inputValue);
if (setting.min !== undefined && numValue < setting.min) { if (setting.min !== undefined && numValue < setting.min) {
newError = `Значение не может быть меньше ${setting.min}`; newError = t('settings-valueTooSmall') + setting.min;
} }
else if (setting.max !== undefined && numValue > setting.max) { else if (setting.max !== undefined && numValue > setting.max) {
newError = `Значение не может быть больше ${setting.max}`; newError = t('settings-valueTooLarge') + setting.max;
} }
if (!newError) { if (!newError) {
@ -172,9 +215,10 @@ const SettingItem = React.memo(({ setting, value, onChange, onValidationError }:
type="button" type="button"
size="small" size="small"
title={setting.defaultValue || ''} title={setting.defaultValue || ''}
onClick={setting.action} onClick={handleButtonAction}
/> />
)} )}
{setting.type === 'hidden' && null}
</SettingBlock> </SettingBlock>
); );
}); });

View file

@ -1,13 +1,13 @@
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style"; import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
import { MainButton } from "../UI/buttons/mainButton"; import { MainButton } from "../UI/buttons/mainButton";
import {useCallback, useEffect, useMemo, useState} from "react"; import {useCallback, useEffect, useMemo, useState} from "react";
import styled from "styled-components"; import styled, {useTheme} from "styled-components";
import {Setting, useSettingsStore} from "../../store/settings/settingsStore"; import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
import {SettingItem} from "./SettingItem"; import {SettingItem} from "./SettingItem";
import {IconComponent} from "../../const/buttons/icons"; import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names"; import {buttonNames} from "../../const/buttons/names";
import {ModalMap} from "../modalwindows/windowsType"; import {useTranslation} from "../../utils/i18n/hooks";
import {workSpaceNames} from "../../const/workspace/names";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@ -49,6 +49,16 @@ const SectionLine = styled.span`
const CloseBtn = styled.span` const CloseBtn = styled.span`
cursor: pointer; cursor: pointer;
` `
const SettingsButtons = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
span{
display: flex;
gap: 12px;
}
`
export const Settings = ({ onClose }: Props) => { export const Settings = ({ onClose }: Props) => {
const settingsConfig = useSettingsStore(state => state.settingsConfig); const settingsConfig = useSettingsStore(state => state.settingsConfig);
const settingsValues = useSettingsStore(state => state.settingsValues); const settingsValues = useSettingsStore(state => state.settingsValues);
@ -59,10 +69,16 @@ export const Settings = ({ onClose }: Props) => {
const applyChanges = useSettingsStore(state => state.applyChanges); const applyChanges = useSettingsStore(state => state.applyChanges);
const discardChanges = useSettingsStore(state => state.discardChanges); const discardChanges = useSettingsStore(state => state.discardChanges);
const getSetting = useSettingsStore(state => state.getSetting); const getSetting = useSettingsStore(state => state.getSetting);
const resetToDefaults = useSettingsStore(state => state.resetToDefaults);
const initializeSettings = useSettingsStore(state => state.initializeSettings); const initializeSettings = useSettingsStore(state => state.initializeSettings);
const updateSettingsConfig = useSettingsStore(state => state.updateSettingsConfig);
const theme = useTheme()
const { t, language } = useTranslation('common');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({}); const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [currentLanguage, setCurrentLanguage] = useState(language);
// Проверка действий
const handleValidationError = useCallback((fieldId: string, error: string | null) => { const handleValidationError = useCallback((fieldId: string, error: string | null) => {
setFieldErrors(prev => { setFieldErrors(prev => {
const newErrors = { ...prev }; const newErrors = { ...prev };
@ -77,22 +93,33 @@ export const Settings = ({ onClose }: Props) => {
const settingsBySection = useMemo(() => { const settingsBySection = useMemo(() => {
return settingsConfig.reduce((acc, setting) => { return settingsConfig.reduce((acc, setting) => {
if (!acc[setting.sectionId]) { // Применяем переводы к настройкам
acc[setting.sectionId] = []; const translatedSetting = {
}
acc[setting.sectionId].push({
...setting, ...setting,
sectionId: t(setting.sectionId),
label: t(setting.label),
defaultValue: typeof setting.defaultValue === 'string' && setting.defaultValue.startsWith('settings-') ? t(setting.defaultValue) : setting.defaultValue,
options: setting.options ? setting.options.map((option: any) => ({
...option,
label: option.label.startsWith('settings-') ? t(option.label) : option.label
})) : setting.options,
value: settingsValues[setting.id] ?? setting.defaultValue value: settingsValues[setting.id] ?? setting.defaultValue
}); };
if (!acc[translatedSetting.sectionId]) {
acc[translatedSetting.sectionId] = [];
}
acc[translatedSetting.sectionId].push(translatedSetting);
return acc; return acc;
}, {} as Record<string, Setting[]>); }, {} as Record<string, Setting[]>);
}, [settingsConfig, settingsValues]); }, [settingsConfig, settingsValues, t]);
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]); const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
useEffect(() => { useEffect(() => {
updateSettingsConfig();
initializeSettings(); initializeSettings();
}, [initializeSettings]); }, [updateSettingsConfig, initializeSettings]);
useEffect(() => { useEffect(() => {
if (sections.length > 0 && !activeSection) { if (sections.length > 0 && !activeSection) {
@ -100,10 +127,15 @@ export const Settings = ({ onClose }: Props) => {
} }
}, [sections, activeSection, setActiveSection]); }, [sections, activeSection, setActiveSection]);
// Обновляем текущий язык при изменении
useEffect(() => {
setCurrentLanguage(language);
}, [language]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
const hasErrors = Object.values(fieldErrors).some(error => error?.trim()); const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
if (hasErrors) { if (hasErrors) {
alert('Исправьте ошибки перед сохранением'); alert(t('settings-fixErrors'));
return; return;
} }
@ -114,13 +146,24 @@ export const Settings = ({ onClose }: Props) => {
}); });
if (emptyRequiredFields.length > 0) { if (emptyRequiredFields.length > 0) {
alert('Заполните все обязательные поля'); alert(t('settings-fillRequired'));
return; return;
} }
// Проверяем, изменился ли язык
const newLanguage = pendingChanges['language'] ?? settingsValues['language'];
const languageChanged = newLanguage && newLanguage !== currentLanguage;
applyChanges(); applyChanges();
onClose(); onClose();
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose]);
// Если язык изменился, перезагружаем приложение
if (languageChanged) {
setTimeout(() => {
window.location.reload();
}, 100);
}
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose, t, currentLanguage]);
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
discardChanges(); discardChanges();
@ -128,10 +171,31 @@ export const Settings = ({ onClose }: Props) => {
onClose(); onClose();
}, [discardChanges, onClose]); }, [discardChanges, onClose]);
const handleReset = () => {
if (window.confirm(t('settings-resetConfirm'))) {
resetToDefaults();
}
};
// Обработка нажатий клавиш
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter')
handleSave();
if (e.key === 'Escape')
handleCancel()
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleSave, handleCancel]);
return ( return (
<ModalContainer style={{ height: '60vh', width: '50vw'}}> <ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle> <ModalTitle>
<h3>Настройки</h3> <h3>{t('settings-title')}</h3>
<CloseBtn> <CloseBtn>
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} /> <IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
</CloseBtn> </CloseBtn>
@ -168,13 +232,32 @@ export const Settings = ({ onClose }: Props) => {
</ModalContent> </ModalContent>
<ButtonContainer> <ButtonContainer>
<SettingsButtons>
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Сохранить' title={t('settings-resetSettings')}
onClick={handleSave} onClick={handleReset}
color={theme.offlineColor}
hoverColor={theme.borderColor}
/> />
<span>
<MainButton
type="button"
size="small"
title={t('settings-cancel')}
onClick={handleCancel}
color={theme.offlineColor}
hoverColor={theme.borderColor}
/>
<MainButton
type="button"
size="small"
title={t('settings-save')}
onClick={handleSave}
/>
</span>
</SettingsButtons>
</ButtonContainer> </ButtonContainer>
</ModalContainer> </ModalContainer>
); );

View file

@ -6,6 +6,7 @@ import styled, {useTheme} from "styled-components"
import ThemeButton from "../../../UI/buttons/toggletheme" import ThemeButton from "../../../UI/buttons/toggletheme"
import {buttonNames} from "../../../../const/buttons/names"; import {buttonNames} from "../../../../const/buttons/names";
import {IconComponent} from "../../../../const/buttons/icons"; import {IconComponent} from "../../../../const/buttons/icons";
import {useTranslation} from "../../../../utils/i18n/hooks";
type Props = { type Props = {
from: 'reg' | 'helloWindow' | 'workSpace' from: 'reg' | 'helloWindow' | 'workSpace'
@ -112,7 +113,7 @@ const LinkBlock = styled.a`
const Copyright = styled.div` const Copyright = styled.div`
width: 100%; width: 100%;
text-align: start; text-align: center;
color: ${(props => props.theme.offlineColor)}; color: ${(props => props.theme.offlineColor)};
` `
@ -158,6 +159,7 @@ const MainTitle = styled.div`
export default function About({ from, onBack }: Props) { export default function About({ from, onBack }: Props) {
const { setWorkModule } = useWorkSpaceStore() const { setWorkModule } = useWorkSpaceStore()
const {t} = useTranslation('common')
const getBackHandler = () => { const getBackHandler = () => {
switch (from) { switch (from) {
case 'reg': case 'reg':
@ -177,11 +179,12 @@ export default function About({ from, onBack }: Props) {
return ( return (
<Wrapper> <Wrapper>
<Content> <Content>
<MainTitle> <MainTitle>
<div className={'title'}> <div className={'title'}>
<img src="./assets/images/gossys.png" alt="" /> <img src="./assets/images/gossys.png" alt={t('about-logoAlt')} />
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}><h1>О приложении</h1></div> <div style={{width: '100%', display: "flex", justifyContent: 'center'}}>
<h1>{t('about-title')}</h1>
</div>
</div> </div>
<p className={'version'}>v 0.0.3</p> <p className={'version'}>v 0.0.3</p>
</MainTitle> </MainTitle>
@ -189,15 +192,15 @@ export default function About({ from, onBack }: Props) {
<AboutLine/> <AboutLine/>
<Title> <Title>
<p>ГоNets — минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.</p> <p>{t('about-description')}</p>
</Title> </Title>
<Listing> <Listing>
<p>Возможности:</p> <p>{t('about-featuresTitle')}:</p>
<List> <List>
<li>Визуализация активных сетевых ресурсов</li> <li>{t('about-feature1')}</li>
<li>Настройка параметров локальных и удалённых сетевых ресурсов</li> <li>{t('about-feature2')}</li>
<li>Управление взаимодействием ресурсов с Клиентом ГoNets</li> <li>{t('about-feature3')}</li>
</List> </List>
</Listing> </Listing>
@ -205,26 +208,28 @@ export default function About({ from, onBack }: Props) {
<svg className="icon" viewBox="0 0 24 24"> <svg className="icon" viewBox="0 0 24 24">
<path d="M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2"/> <path d="M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2"/>
</svg> </svg>
<p>Программное обеспечение исключительно для ознакомления и домашнего использования.</p> <p>{t('about-disclaimer')}</p>
<a href={'https://gossys.dev/pages/projects/gonets'}>Перейти на ГоNets-PRO</a> <a href={'https://gossys.dev/pages/projects/gonets'}>
{t('about-goToPro')}
</a>
</SubTitle> </SubTitle>
<Links> <Links>
<LinkBlock href={'https://gossys.dev/pages/projects/gonets'}> <LinkBlock href={'https://gossys.dev/pages/projects/gonets'}>
Домашняя страницa {t('about-homepage')}
</LinkBlock> </LinkBlock>
<LinkBlock href={'https://wiki.gossys.dev/ru/gonets'}> <LinkBlock href={'https://wiki.gossys.dev/ru/gonets'}>
Документация {t('about-documentation')}
</LinkBlock> </LinkBlock>
<LinkBlock href={'https://wiki.gossys.dev/ru/lic/GosysPUB'}> <LinkBlock href={'https://wiki.gossys.dev/ru/lic/GosysPUB'}>
Лицензия {t('about-license')}
</LinkBlock> </LinkBlock>
</Links> </Links>
<Copyright> <Copyright>
Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены. {t('about-copyright')}
</Copyright> </Copyright>
<ButtonContainer> <ButtonContainer>
@ -235,7 +240,6 @@ export default function About({ from, onBack }: Props) {
name={buttonNames[12]} name={buttonNames[12]}
onClick={backHandler} onClick={backHandler}
/> />
)} )}
{from == 'reg' && {from == 'reg' &&
@ -243,7 +247,6 @@ export default function About({ from, onBack }: Props) {
} }
</ButtonContainer> </ButtonContainer>
</Content> </Content>
</Wrapper> </Wrapper>
) )
} }

View file

@ -3,6 +3,7 @@
import { useWorkSpaceStore } from "../../../../store" import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names" import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names" import { buttonNames } from "../../../../const/buttons/names"
import {useTranslation} from "../../../../utils/i18n/hooks";
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@ -49,27 +50,30 @@
` `
export const HelloWindow = () => { export const HelloWindow = () => {
const {t} = useTranslation('common')
const { setWorkModule } = useWorkSpaceStore() const { setWorkModule } = useWorkSpaceStore()
return ( return (
<Wrapper> <Wrapper>
<Container className="unselectable"> <Container className="unselectable">
<Title> <Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/> <img
src="./assets/images/logo.svg"
alt={t('main-logoAlt')}
style={{width: '152px'}}
/>
</Title> </Title>
<ButtonWrapper> <ButtonWrapper>
<ButtonContainer> <ButtonContainer>
<MainButton <MainButton
title="Настройки сети" title={t('main-networkSettings')}
icon={buttonNames[3]} icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }} iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])} onClick={() => setWorkModule(workSpaceNames[3])}
/> />
<MainButton <MainButton
title="О программе" title={t('main-about')}
icon={buttonNames[4]} icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }} iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[4])} onClick={() => setWorkModule(workSpaceNames[4])}

View file

@ -0,0 +1,85 @@
import React, {useState} from 'react';
import styled, {keyframes} from 'styled-components';
import {useTranslation} from "../../../../../../utils/i18n/hooks";
interface Props {
value: string;
setIpScanning: (value: string) => void;
loading: boolean;
isValid: boolean;
setIsValid: (value: boolean) => void;
}
const Input = styled.input`
height: 40px;
width: 210px;
font-size: 20px;
text-align: center;
background-color: transparent;
border: none;
outline: none;
border-bottom: 2px solid ${props => props.theme.offlineColor};
color: ${props => props.theme.textColor};
transition: all 0.3s ease;
&:focus {
border-bottom: 2px solid ${props => props.theme.accent};
width: 270px;
}
&::placeholder {
color: ${props => props.theme.offlineColor};
transition: all 0.3s ease;
}
&:focus::placeholder {
opacity: 0;
}
&:disabled {
opacity: 0;
}
`
const errorVisible = keyframes`
0%{opacity: 0}
100%{opacity: 1}
`;
const ErrorMessage = styled.span`
margin-top: 6px;
color: ${props=>props.theme.errorColor};
animation: ${errorVisible} .4s linear;
`
const IpScanning = ({value, setIpScanning, loading, isValid, setIsValid}: Props) => {
const {t} = useTranslation('common')
const validateInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
const allowedKeys = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'.', '/',
'Backspace', 'Delete',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'
];
if (!allowedKeys.includes(e.key)) {
e.preventDefault();
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIpScanning(e.target.value);
}
return (
<>
<Input
value={value}
onChange={handleChange}
placeholder={t('ipInput-placeholder')}
onKeyDown={validateInput}
disabled={loading}
/>
{!isValid && !loading && (
<ErrorMessage>{t('ipInput-error')}</ErrorMessage>
)}
</>
);
};
export default IpScanning;

View file

@ -4,6 +4,7 @@ import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore"; import {DeviceUser} from "../../../../../../store/useDeviceStore";
import {usePaginateSetting} from "../../../../../../hooks/useSettings"; import {usePaginateSetting} from "../../../../../../hooks/useSettings";
import Pagination from "../../../../../UI/Pagination"; import Pagination from "../../../../../UI/Pagination";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button` const ScanBtn = styled.button`
position: relative; position: relative;
@ -27,6 +28,7 @@ interface ListUsersProps {
} }
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => { const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
const activeDevices = devices.filter(device => device.status === 'online'); const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online'); const inactiveDevices = devices.filter(device => device.status !== 'online');
@ -55,11 +57,10 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
setCurrentPage(page); setCurrentPage(page);
} }
return ( return (
<> <>
<ScanBtn onClick={handleScan} disabled={loading}> <ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'} {loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn> </ScanBtn>
{devices.length > 0 ? ( {devices.length > 0 ? (
@ -70,7 +71,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
}}> }}>
{/* Колонка активных устройств */} {/* Колонка активных устройств */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные узлы ({activeDevices.length})</h3> <h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>
{t('listUsers-activeNodes')} ({activeDevices.length})
</h3>
{currentActiveDevices.map((device, index) => ( {currentActiveDevices.map((device, index) => (
<DeviceCard <DeviceCard
key={index} key={index}
@ -81,7 +84,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
</div> </div>
{/* Колонка неактивных устройств */} {/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные узлы ({inactiveDevices.length})</h3> <h3 style={{ color: '#ababab', marginBottom:'12px' }}>
{t('listUsers-inactiveNodes')} ({inactiveDevices.length})
</h3>
{currentInactiveDevices.map((device, index) => ( {currentInactiveDevices.map((device, index) => (
<DeviceCard <DeviceCard
key={index} key={index}
@ -91,8 +96,8 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
))} ))}
</div> </div>
</div> </div>
) : scanInitiated ? ( ) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>Узлы не найдены</p> <p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null} ) : null}
{totalPages > 1 && ( {totalPages > 1 && (
<Pagination <Pagination

View file

@ -1,17 +1,22 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useMemo, useState} from 'react';
import { DeviceUser } from '../../../../../../store/useDeviceStore'; import { DeviceUser } from '../../../../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components'; import styled from 'styled-components';
import {IconComponent} from "../../../../../../const/buttons/icons"; import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names"; import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType"; import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store"; import {useModalContext} from "../../../../../../store";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
import Pagination from "../../../../../UI/Pagination"; import Pagination from "../../../../../UI/Pagination";
import {useSettingsStore} from "../../../../../../store/settings/settingsStore"; import {
import {useNameSetting, usePaginateSetting} from "../../../../../../hooks/useSettings"; useDisabledColumns,
useNameSetting,
useOrdersColumns,
usePaginateSetting
} from "../../../../../../hooks/useSettings";
interface TableScannerProps {
devices: DeviceUser[];
}
const TableContainer = styled.div` const TableContainer = styled.div`
width: 100%; width: 100%;
@ -65,8 +70,23 @@ const StatusBadge = styled.span<{ status:string }>`
border-radius:4px; border-radius:4px;
` `
const TableScanner = ({devices}:TableScannerProps) => { export const TableScanner = ({ devices }: TableScannerProps) => {
const { openModal } = useModalContext(); const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const ordersColumns = useOrdersColumns();
const disabledColumns = useDisabledColumns();
const paginateSetting = usePaginateSetting();
const {t} = useTranslation('common');
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
console.log(ordersColumns);
useEffect(() => {
setDevicesPerPage(paginateSetting);
}, [paginateSetting]);
const openModalNote = (e: React.MouseEvent, device: DeviceUser) => { const openModalNote = (e: React.MouseEvent, device: DeviceUser) => {
e.stopPropagation(); e.stopPropagation();
@ -75,17 +95,89 @@ const TableScanner = ({devices}:TableScannerProps) => {
currentNote: device.note || '' currentNote: device.note || ''
}); });
}; };
const nameSetting = useNameSetting();
const defaultOrder = useMemo(() => ([
t('settings-status'),
t('settings-name'),
t('settings-vendor'),
t('settings-ipAddress'),
t('settings-openPorts'),
t('settings-lastActivity'),
t('settings-note'),
t('settings-action'),
]), [t]);
const normalizedOrders = useMemo(
() => (Array.isArray(ordersColumns) && ordersColumns.length ? ordersColumns : defaultOrder),
[ordersColumns, defaultOrder]
);
const normalizedDisabled = useMemo(
() => (Array.isArray(disabledColumns) ? disabledColumns : []),
[disabledColumns]
);
const visibleColumns = useMemo(() => {
const normalizeColumnName = (column: string) => column?.toLowerCase();
const columnToKey = (column: string) => {
const col = normalizeColumnName(column);
switch (col) {
case 'settings-status':
case normalizeColumnName(t('settings-status')):
case 'онлайн':
case 'status':
return 'status';
case 'settings-name':
case normalizeColumnName(t('settings-name')):
case 'имя':
case 'name':
return 'name';
case 'settings-vendor':
case normalizeColumnName(t('settings-vendor')):
case 'производитель':
case 'vendor':
return 'vendor';
case 'settings-ipaddress':
case normalizeColumnName(t('settings-ipAddress')):
case 'ip адрес':
case 'ip address':
case 'ip':
return 'ip';
case 'settings-openports':
case normalizeColumnName(t('settings-openPorts')):
case 'открытые порты':
case 'open ports':
case 'ports':
return 'ports';
case 'settings-lastactivity':
case normalizeColumnName(t('settings-lastActivity')):
case 'последняя активность':
case 'last activity':
case 'activity':
return 'activity';
case 'settings-note':
case normalizeColumnName(t('settings-note')):
case 'заметка':
case 'note':
return 'note';
case 'settings-action':
case normalizeColumnName(t('settings-action')):
case 'действие':
case 'action':
return 'action';
default:
return '';
}
};
return normalizedOrders
.map((column, index) => ({ column, index }))
.filter(({ index }) => !normalizedDisabled.includes(index))
.map(({ column }) => columnToKey(column))
.filter(Boolean);
}, [normalizedOrders, normalizedDisabled, t]);
// Пагинация // Пагинация
const paginateSetting = usePaginateSetting();
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
useEffect(() => {
setDevicesPerPage(paginateSetting);
}, [paginateSetting]);
const indexOfLastDevice = currentPage * devicesPerPage; const indexOfLastDevice = currentPage * devicesPerPage;
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage; const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
const currentDevices = devices.slice(indexOfFirstDevice, indexOfLastDevice); const currentDevices = devices.slice(indexOfFirstDevice, indexOfLastDevice);
@ -93,48 +185,75 @@ const TableScanner = ({devices}:TableScannerProps) => {
const handlePrevPage = () => { const handlePrevPage = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1); if (currentPage > 1) setCurrentPage(currentPage - 1);
} };
const handleNextPage = () => { const handleNextPage = () => {
if (currentPage < totalPages) setCurrentPage(currentPage + 1); if (currentPage < totalPages) setCurrentPage(currentPage + 1);
} };
const onPageChange = (page: number) => { const onPageChange = (page: number) => {
setCurrentPage(page); setCurrentPage(page);
};
// Рендер ячейки таблицы
const renderTableCell = (column: string, item: DeviceUser) => {
switch (column) {
case 'status':
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
case 'name':
return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>;
case 'vendor':
return <TableCell key="vendor">{item.vendor}</TableCell>;
case 'ip':
return <TableCell key="ip">{item.ip}</TableCell>;
case 'ports':
return <TableCell key="ports">{item.open_ports.join(', ') || '-'}</TableCell>;
case 'activity':
return <TableCell key="activity">{item.last_seen}</TableCell>;
case 'note':
return <TableCell key="note">{item.note || '-'}</TableCell>;
case 'action':
return (
<TableCell key="action">
<button onClick={(e) => openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</button>
</TableCell>
);
default:
return null;
} }
};
return ( return (
<TableContainer> <TableContainer>
<StyledTable> <StyledTable>
<thead> <thead>
<tr> <tr>
<TableHeaderCell></TableHeaderCell> {visibleColumns.map(column => {
<TableHeaderCell>Имя</TableHeaderCell> switch (column) {
<TableHeaderCell>Производитель</TableHeaderCell> case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
<TableHeaderCell>IP адрес</TableHeaderCell> case 'name': return <TableHeaderCell key="name">{t('settings-name')}</TableHeaderCell>;
<TableHeaderCell>Открытые порты</TableHeaderCell> case 'vendor': return <TableHeaderCell key="vendor">{t('settings-vendor')}</TableHeaderCell>;
<TableHeaderCell>Последняя активность</TableHeaderCell> case 'ip': return <TableHeaderCell key="ip">{t('settings-ipAddress')}</TableHeaderCell>;
<TableHeaderCell>Заметка</TableHeaderCell> case 'ports': return <TableHeaderCell key="ports">{t('settings-openPorts')}</TableHeaderCell>;
<TableHeaderCell></TableHeaderCell> case 'activity': return <TableHeaderCell key="activity">{t('settings-lastActivity')}</TableHeaderCell>;
case 'note': return <TableHeaderCell key="note">{t('settings-note')}</TableHeaderCell>;
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
default: return null;
}
})}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{currentDevices.map((item, i) => ( {currentDevices.map((item, index) => (
<TableRow key={i}> <TableRow key={index}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell> {visibleColumns.map(column => renderTableCell(column, item))}
<TableCell>{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell>
<TableCell>{item.note || '-'}</TableCell>
<TableCell>
<button onClick={(e)=>openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</button>
</TableCell>
</TableRow> </TableRow>
))} ))}
</tbody> </tbody>
</StyledTable> </StyledTable>
{totalPages > 1 && ( {totalPages > 1 && (
<Pagination <Pagination
onPageChange={onPageChange} onPageChange={onPageChange}
@ -147,5 +266,3 @@ const TableScanner = ({devices}:TableScannerProps) => {
</TableContainer> </TableContainer>
); );
}; };
export default TableScanner;

View file

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore"; import {DeviceUser} from "../../../../../../store/useDeviceStore";
import TableScanner from "./TableScanner"; import {TableScanner} from "./TableScanner";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button` const ScanBtn = styled.button`
position: relative; position: relative;
@ -26,17 +26,18 @@ interface ListUsersProps {
} }
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => { const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
return ( return (
<> <>
<ScanBtn onClick={() => handleScan()} disabled={loading}> <ScanBtn onClick={() => handleScan()} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'} {loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn> </ScanBtn>
{devices.length > 0 ? ( {devices.length > 0 ? (
<TableScanner devices={devices} /> <TableScanner devices={devices} />
) : scanInitiated ? ( ) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>Узлы не найдены</p> <p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null} ) : null}
</> </>
); );

View file

@ -1,12 +1,15 @@
import { useIpStore } from "../../../../../store"; import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components"; import styled, { keyframes } from "styled-components";
import {useEffect, useState} from "react"; import {useEffect, useState, useTransition} from "react";
import { NetworkService } from "../../../../../api/request"; import { NetworkService } from "../../../../../api/request";
import ListUsers from "./modules/ListUsers"; import ListUsers from "./modules/ListUsers";
import TableUsers from "./modules/TableUsers"; import TableUsers from "./modules/TableUsers";
import DropdownType from "../../../../UI/Dropdown/DropdownType"; import DropdownType from "../../../../UI/Dropdown/DropdownType";
import useNetworkStore from "../../../../../store/useDeviceStore"; import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings"; import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
import IpScanning from "./modules/IpScanning";
import {useTranslation} from "../../../../../utils/i18n/hooks";
type Option ={ type Option ={
value: number, value: number,
@ -52,7 +55,7 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
height: 100%; height: 100%;
width: 100%; width: 100%;
background: ${(props) => props.theme.background}; background: ${(props) => props.theme.background};
padding: 50px; padding: 80px 50px 40px;
box-sizing: border-box; box-sizing: border-box;
gap: 24px; gap: 24px;
transition: justify-content .4s ease; transition: justify-content .4s ease;
@ -60,13 +63,20 @@ const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
overflow-x: hidden; overflow-x: hidden;
position: relative; position: relative;
`; `;
const TitleWrapper = styled.h2` const TopWrapper = styled.div`
position: absolute; position: absolute;
top: 20px; top: 20px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`
const TopTitle = styled.h2`
font-size: 32px; font-size: 32px;
width: max-content; width: max-content;
text-align: center;
` `
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>` const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px; width: 60px;
@ -131,24 +141,39 @@ const ScanningText = styled.span`
export default function UserScanner() { export default function UserScanner() {
const {t} = useTranslation('common')
const { ipAddress } = useIpStore(); const { ipAddress } = useIpStore();
const [ipScanning, setIpScanning] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isValid, setIsValid] = useState(true);
const { devices, setDevices } = useNetworkStore(); const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0); const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
const validateIp = (ip: string) => {
const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
return regex.test(ip);
};
// Сканирование // Сканирование
const handleScan = async (replay: boolean) => { const handleScan = async (replay: boolean) => {
try { try {
if (ipScanning && !validateIp(ipScanning)) {
setIsValid(false);
return;
}
setIsValid(true);
if (!replay) { if (!replay) {
setLoading(true); setLoading(true);
} }
setScanInitiated(true); setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress); const res = await NetworkService.scanUser(ipAddress, ipScanning);
console.log(res); console.log(res)
setDevices(res.data.devices || []); setDevices(res.data.devices || []);
} catch (err) { } catch (err) {
console.error("Ошибка сканирования сети:", err); console.error("Ошибка сканирования сети:", err);
setDevices([]) setDevices([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -173,8 +198,8 @@ export default function UserScanner() {
// Способ отображения // Способ отображения
const viewOptions: Option[] = [ const viewOptions: Option[] = [
{ value: 1, label: 'Таблица' }, { value: 1, label: t('userScanner-visible_table') },
{ value: 2, label: 'Список' }, { value: 2, label: t('userScanner-visible_list') },
]; ];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]); const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => { const handleViewChange = (option: Option) => {
@ -183,7 +208,18 @@ export default function UserScanner() {
return ( return (
<Wrapper scanInitiated={scanInitiated} loading={loading}> <Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов подсети</TitleWrapper>)}
<TopWrapper>
{(!scanInitiated || loading) && (<TopTitle>{t('userScanner-title')}</TopTitle>)}
<IpScanning
value={ipScanning}
setIpScanning={setIpScanning}
loading={loading}
isValid={isValid}
setIsValid={setIsValid}
/>
</TopWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}> <UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span> <span className="center"></span>
<span className="ring1"></span> <span className="ring1"></span>
@ -192,12 +228,12 @@ export default function UserScanner() {
</UserLocate> </UserLocate>
{scanInitiated && !loading && ( {scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" /> <DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
)} )}
{loading ? ( {loading ? (
<LoadingText> <LoadingText>
Сканирование узлов <ScanningText /> {t('userScanner-loading')} <ScanningText />
</LoadingText> </LoadingText>
) : selectedView.value == 1 ? ( ) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} /> <TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />

View file

@ -5,6 +5,7 @@ import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner"; import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore"; import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store"; import {useIpStore} from "../../../../../store";
import {useTranslation} from "../../../../../utils/i18n/hooks";
// Анимации // Анимации
const ring1 = keyframes` const ring1 = keyframes`
@ -57,6 +58,7 @@ const TitleWrapper = styled.h2`
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
font-size: 32px; font-size: 32px;
text-align: center;
` `
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>` const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
@ -136,16 +138,17 @@ const ScanBtn = styled.button<{scanInitiated: boolean}>`
` `
export function WifiScanner() { export function WifiScanner() {
//const { ipAddress } = useIpStore(); const { ipAddress } = useIpStore();
const ipAddress = '192.168.1.147' //const ipAddress = '192.168.1.148'
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false); const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore(); const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null); const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const { t } = useTranslation('common');
const handleScan = async () => { const handleScan = async () => {
setWifiNetworks([]); setWifiNetworks([]);
setCurrentNetwork(null) setCurrentNetwork(null);
setLoading(true); setLoading(true);
setScanInitiated(true); setScanInitiated(true);
try { try {
@ -153,7 +156,7 @@ export function WifiScanner() {
const networks = res.data.access_points || []; const networks = res.data.access_points || [];
setWifiNetworks(networks); setWifiNetworks(networks);
} catch (err) { } catch (err) {
console.error("Ошибка сканирования сети:", err); console.error(t('wifiScanner-scanError'), err);
setWifiNetworks([]); setWifiNetworks([]);
} finally { } finally {
setLoading(false); setLoading(false);
@ -162,7 +165,7 @@ export function WifiScanner() {
return ( return (
<Wrapper scanInitiated={scanInitiated} loading={loading}> <Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование точек доступа Wi-Fi сети</TitleWrapper> <TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}> <UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span> <span className="center circel"></span>
@ -189,11 +192,15 @@ export function WifiScanner() {
{loading ? ( {loading ? (
<LoadingText> <LoadingText>
Сканирование точек доступа <ScanningText/> {t('wifiScanner-scanning')} <ScanningText/>
</LoadingText> </LoadingText>
) : ( ) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }> <ScanBtn
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'} onClick={handleScan}
disabled={loading}
scanInitiated={wifiNetworks.length > 0}
>
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
</ScanBtn> </ScanBtn>
)} )}
</Wrapper> </Wrapper>

View file

@ -10,6 +10,7 @@ import SignalSVG from "../../../../../UI/signalSvg";
import { useTheme } from 'styled-components'; import { useTheme } from 'styled-components';
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore"; import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
import {NetworkService} from "../../../../../../api/request"; import {NetworkService} from "../../../../../../api/request";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const Modal = styled.div` const Modal = styled.div`
display: flex; display: flex;
@ -114,6 +115,7 @@ const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onC
const [signal, setSignal] = useState(1); const [signal, setSignal] = useState(1);
const { wifiNetworks } = useNetworkStore(); const { wifiNetworks } = useNetworkStore();
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const { t } = useTranslation('common');
useEffect(() => { useEffect(() => {
const getStatus = async () => { const getStatus = async () => {
@ -174,7 +176,6 @@ const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onC
<ModalTitle> <ModalTitle>
<h4>{network.ssid}</h4> <h4>{network.ssid}</h4>
<div className={'functionalIcons'}> <div className={'functionalIcons'}>
<span onClick={openModalNote} style={{cursor: "pointer"}}> <span onClick={openModalNote} style={{cursor: "pointer"}}>
<IconComponent name={buttonNames[16]} width="30" height="30" /> <IconComponent name={buttonNames[16]} width="30" height="30" />
</span> </span>
@ -185,38 +186,40 @@ const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onC
</ModalTitle> </ModalTitle>
<ModalContent> <ModalContent>
<ModalRow> <ModalRow>
<h6>BSSID:</h6> <h6>{t('wifiModal-bssid')}:</h6>
<p>{network.bssid}</p> <p>{network.bssid}</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Производитель:</h6> <h6>{t('wifiModal-vendor')}:</h6>
<p>{network.vendor}</p> <p>{network.vendor || t('wifiModal-unknown')}</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Сигнал:</h6> <h6>{t('wifiModal-signal')}:</h6>
<p>{network.signal} dBm</p> <p>{network.signal} dBm</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Канал:</h6> <h6>{t('wifiModal-channel')}:</h6>
<p>{network.channel}</p> <p>{network.channel}</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Шифрование:</h6> <h6>{t('wifiModal-encryption')}:</h6>
<p>{network.encryption}</p> <p>{network.encryption || t('wifiModal-none')}</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Частота:</h6> <h6>{t('wifiModal-frequency')}:</h6>
<p>{network.frequency}</p> <p>{network.frequency} MHz</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Скрытая сеть:</h6> <h6>{t('wifiModal-hidden')}:</h6>
<p>{network.hidden ? 'Да' : 'Нет'}</p> <p>{network.hidden ? t('common-yes') : t('common-no')}</p>
</ModalRow> </ModalRow>
<ModalRow> <ModalRow>
<h6>Заметка:</h6> <h6>{t('wifiModal-note')}:</h6>
<p>{currentNetwork.note ? currentNetwork.note : '-'}</p> <p>{currentNetwork.note || '-'}</p>
</ModalRow> </ModalRow>
<ModalConnect onClick={openModalNetwork}>{isConnected ? 'Отключиться' : 'Подключиться'}</ModalConnect> <ModalConnect onClick={openModalNetwork}>
{isConnected ? t('wifiModal-disconnect') : t('wifiModal-connect')}
</ModalConnect>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false) setHasInited(false)
initializationAttempt.current = 0; initializationAttempt.current = 0;
const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`); const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`);
let connectionTimeout; let connectionTimeout;
setSocket(newSocket); setSocket(newSocket);

View file

@ -69,7 +69,7 @@ export const ToolBar = () => {
</IconContainer> </IconContainer>
</Button> </Button>
<ThemeButton /> {/*<ThemeButton />*/}
<Button> <Button>
<IconContainer> <IconContainer>

View file

@ -11,6 +11,7 @@ import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names" import { workSpaceNames } from "../../../../const/workspace/names"
import { IconComponent } from "../../../../const/buttons/icons" import { IconComponent } from "../../../../const/buttons/icons"
import { buttonNames } from "../../../../const/buttons/names" import { buttonNames } from "../../../../const/buttons/names"
import {useTranslation} from "../../../../utils/i18n/hooks";
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
@ -107,6 +108,7 @@ export const DevicesBar = () => {
const {devices, addDevice, deleteDevice} = useDevicesStore() const {devices, addDevice, deleteDevice} = useDevicesStore()
const {isOpen, collapseMenu} = useMenu(menusNames[1]); const {isOpen, collapseMenu} = useMenu(menusNames[1]);
const {setWorkModule} = useWorkSpaceStore() const {setWorkModule} = useWorkSpaceStore()
const {t} = useTranslation('common');
const [currentDevice, setCurrentDevice] = useState<CurrentDevice>({ const [currentDevice, setCurrentDevice] = useState<CurrentDevice>({
address: 'localhost', address: 'localhost',
@ -115,7 +117,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => { const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`); const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
testSocket.close(); testSocket.close();
resolve(false); resolve(false);
@ -173,7 +175,7 @@ export const DevicesBar = () => {
const isAvailable = await checkWebSocketAvailability(ip); const isAvailable = await checkWebSocketAvailability(ip);
if (!isAvailable) { if (!isAvailable) {
openModal(ModalMap['3'], { openModal(ModalMap['3'], {
message: `Не удалось подключиться к серверу по адресу ${ip}`, message: t('nodes-connectionError') + ip,
}); });
return; return;
} }
@ -191,23 +193,25 @@ export const DevicesBar = () => {
return ( return (
<Container> <Container>
<Title> <Title>
<p className="unselectable">Доступные узлы</p> <p className="unselectable">{t('nodes-availableNodes')}</p>
</Title> </Title>
<DeviceBlock> <DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}> <DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Корневой адрес</p> <p>{t('nodes-rootAddress')}</p>
</DeviceControl> </DeviceControl>
{currentDevice.hostname == '' ? ( {currentDevice.hostname == '' ? (
<Text className="paddingP">Название: Загрузка<AnimDot /></Text> <Text className="paddingP">
{t('nodes-name')}: {t('nodes-loading')}<AnimDot/>
</Text>
) : ( ) : (
<p className="paddingP">Название: {currentDevice.hostname}</p> <p className="paddingP">{t('nodes-name')}: {currentDevice.hostname}</p>
)} )}
</DeviceBlock> </DeviceBlock>
<DevicesContainer> <DevicesContainer>
<DevicesHeader className="unselectable" onClick={collapseMenu}> <DevicesHeader className="unselectable" onClick={collapseMenu}>
<p>Дополнительные узлы</p> <p>{t('nodes-additionalNodes')}</p>
<IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]}/> <IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]}/>
</DevicesHeader> </DevicesHeader>
@ -216,19 +220,19 @@ export const DevicesBar = () => {
devices.map((device, index) => ( devices.map((device, index) => (
<DeviceBlock key={index}> <DeviceBlock key={index}>
<DeviceControl className="unselectable" onClick={() => switchWs(device.address)}> <DeviceControl className="unselectable" onClick={() => switchWs(device.address)}>
<p>Узел {device.number}</p> <p>{t('nodes-node')} {device.number}</p>
<IconComponent <IconComponent
width="20" width="20"
height="20" height="20"
name={buttonNames[2]} name={buttonNames[2]}
setColor="white" setColor="white"
onClick={(e: any) => { onClick={(e: any) => {
e.stopPropagation(); // Предотвращаем всплытие события e.stopPropagation();
handleDelete(device.number, device.address); handleDelete(device.number, device.address);
}} }}
/> />
</DeviceControl> </DeviceControl>
<p className="paddingP">Адрес: {device.address}</p> <p className="paddingP">{t('nodes-address')}: {device.address}</p>
</DeviceBlock> </DeviceBlock>
)) ))
} }
@ -237,7 +241,6 @@ export const DevicesBar = () => {
+ +
</PlusBlock> </PlusBlock>
</DevicesContent> </DevicesContent>
</DevicesContainer> </DevicesContainer>
</Container> </Container>
) )

View file

@ -2,4 +2,8 @@ interface Window {
electron: { electron: {
quitApp: () => void; quitApp: () => void;
}; };
electronAPI: {
showSaveDialog: (options: Electron.SaveDialogOptions) => Promise<Electron.SaveDialogReturnValue>;
writeFile: (path: string, data: string) => Promise<{ success: boolean }>;
};
} }

View file

@ -1,4 +1,5 @@
import {useSettingsStore} from "../store/settings/settingsStore"; import {useSettingsStore} from "../store/settings/settingsStore";
import {defaultLanguage, Language} from "../utils/i18n/config";
export const useNameSetting = () => { export const useNameSetting = () => {
return useSettingsStore(state => state.getSettingValue('netnames-nodes')) as string; return useSettingsStore(state => state.getSettingValue('netnames-nodes')) as string;
@ -8,6 +9,11 @@ export const usePaginateSetting = () => {
return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20)); return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20));
}; };
export const useThemeSettings = () => {
return useSettingsStore(state => state.getSettingValue('theme'));
};
//Интервальное сканирование под узлов
export const useIntervalBoolean = () => { export const useIntervalBoolean = () => {
return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean; return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean;
}; };
@ -15,6 +21,17 @@ export const useIntervalTime = () => {
return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60)); return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60));
}; };
export const useThemeSetting = () => { // Кастомизация таблиц
return useSettingsStore(state => state.getSettingValue('theme')) as 'light' | 'dark' | 'system'; export const useOrdersColumns = () => {
return useSettingsStore(state => state.getSettingValue('table-columns-order')) as string[];
};
export const useDisabledColumns = () => {
return useSettingsStore(state => state.getSettingValue('table-columns-disabled')) as number[];
};
export const useLanguage = () => {
return useSettingsStore(
state => state.getSettingValue<Language>('language')
) ?? defaultLanguage;
}; };

View file

@ -9,7 +9,7 @@ interface IpAddressState {
export const useIpStore = create<IpAddressState>()( export const useIpStore = create<IpAddressState>()(
persist( persist(
(set)=>({ (set)=>({
ipAddress: '192:168.1.147', ipAddress: '192:168.1.0',
setIpAddress: (newIp: string) => set({ ipAddress: newIp }), setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
}), }),
{ {

View file

@ -1,25 +1,30 @@
export const appearanceSettings = [
const createAppearanceSettings = () => {
return [
{ {
id: 'theme', id: 'theme',
sectionId: 'Внешний вид', sectionId: 'settings-sections-appearance',
label: 'Цветовая тема', label: 'settings-colorTheme',
type: 'select', type: 'select',
defaultValue: 'light', defaultValue: 'light',
options: [ options: [
{ value: 'light', label: 'Светлая' }, { value: 'light', label: 'settings-light' },
{ value: 'dark', label: 'Тёмная' }, { value: 'dark', label: 'settings-dark' },
{ value: 'system', label: 'Как в системе' } { value: 'system', label: 'settings-system' }
], ],
}, },
{ {
id: 'language', id: 'language',
sectionId: 'Внешний вид', sectionId: 'settings-sections-appearance',
label: 'Язык интерфейса', label: 'settings-interfaceLanguage',
type: 'select', type: 'select',
defaultValue: 'ru', defaultValue: 'ru',
options: [ options: [
{ value: 'ru', label: 'Русский' }, { value: 'ru', label: 'settings-russian' },
{ value: 'en', label: 'Английский' }, { value: 'en', label: 'settings-english' },
], ],
}, },
] as const; ] as const;
};
export { createAppearanceSettings };

View file

@ -1,16 +1,55 @@
export const networkSettings = [ import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNetworkSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{ {
id: 'clean-network', id: 'clean-network',
type: 'button', type: 'button',
label: 'Очистить данные сессии', label: 'settings-clearSessionData',
sectionId: 'Точки доступа', sectionId: 'settings-sections-accessPoints',
defaultValue: 'Очистить', defaultValue: 'settings-clear',
action: async ()=>{
try {
const res = await NetworkService.clearNetwork(ipAddress);
console.log(res);
}catch (e){
console.error(e);
alert('settings-dataNotDeleted' + e)
}
},
}, },
{ {
id: 'export-network', id: 'export-network',
type: 'button', type: 'button',
label: 'Экспорт данных точек доступа', label: 'settings-exportNetworkData',
sectionId: 'Точки доступа', sectionId: 'settings-sections-accessPoints',
defaultValue: 'Экспортировать', defaultValue: 'settings-export',
action: async () => {
try {
const response = await NetworkService.exportNetwork(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: 'settings-saveNetworksTitle',
defaultPath: "network_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert('settings-exportSuccess');
}
} catch (e: any) {
alert('settings-exportError' + e.message);
}
},
}, },
] as const; ] as const;
};
export { createNetworkSettings };

View file

@ -1,15 +1,21 @@
export const nodesSettings = [ import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNodesSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{ {
id: 'rescan-nodes', id: 'rescan-nodes',
sectionId: 'Узлы подсети', sectionId: 'settings-sections-subnetNodes',
label: 'Интервальное сканирование', label: 'settings-intervalScanning',
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
}, },
{ {
id: 'interval-nodes', id: 'interval-nodes',
sectionId: 'Узлы подсети', sectionId: 'settings-sections-subnetNodes',
label: 'Интервал сканирования узлов (мин)', label: 'settings-scanInterval',
type: 'number', type: 'number',
defaultValue: 60, defaultValue: 60,
min: 1, min: 1,
@ -17,43 +23,97 @@ export const nodesSettings = [
}, },
{ {
id: 'netnames-nodes', id: 'netnames-nodes',
sectionId: 'Узлы подсети', sectionId: 'settings-sections-subnetNodes',
label: 'Отображаемые имена при отсутствии', label: 'settings-displayNames',
type: 'select', type: 'select',
defaultValue: 'ip', defaultValue: 'ip',
options: [ options: [
{ value: 'ip', label: 'IP адрес' }, { value: 'ip', label: 'settings-ipAddress' },
{ value: 'unknown', label: 'Неизвестно' }, { value: 'unknown', label: 'settings-unknown' },
], ],
}, },
{ {
id: 'paginate-nodes', id: 'paginate-nodes',
type: 'number', type: 'number',
label: 'Количество строк в таблице', label: 'settings-rowsPerPage',
sectionId: 'Узлы подсети', sectionId: 'settings-sections-subnetNodes',
defaultValue: 20, defaultValue: 20,
min: 1, min: 1,
}, },
{ {
id: 'customize-nodes', id: 'customize-nodes',
type: 'button', type: 'button',
label: 'Отображение узлов', label: 'settings-displaySettings',
sectionId: 'Узлы подсети', sectionId: 'settings-sections-subnetNodes',
defaultValue: 'Настроить', defaultValue: 'settings-configure',
}, },
{ {
id: 'clean-nodes', id: 'clean-nodes',
type: 'button', type: 'button',
label: 'Очистить данные сессии', label: 'settings-clearSessionData',
sectionId: 'Узлы подсети', sectionId: 'settings-sections-subnetNodes',
defaultValue: 'Очистить', defaultValue: 'settings-clear',
action: async ()=>{
try {
const res = await NetworkService.clearSubnet(ipAddress);
console.log(res)
}catch (e){
console.error(e);
alert('settings-dataNotDeleted' + e)
}
},
}, },
{ {
id: 'export-nodes', id: "export-nodes",
type: 'button', type: "button",
label: 'Экспорт данных узлов', label: "settings-exportData",
sectionId: 'Узлы подсети', sectionId: "settings-sections-subnetNodes",
defaultValue: 'Экспортировать', defaultValue: "settings-export",
}, action: async () => {
] as const; try {
const response = await NetworkService.exportDevices(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: "settings-saveNodesTitle",
defaultPath: "nodes_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert("settings-exportSuccess");
}
} catch (e: any) {
alert("settings-exportError" + e.message);
}
},
},
{
id: 'table-columns-order',
type: 'hidden',
label: '',
sectionId: 'settings-sections-subnetNodes',
defaultValue: [
'settings-status',
'settings-name',
'settings-vendor',
'settings-ipAddress',
'settings-openPorts',
'settings-lastActivity',
'settings-note',
'settings-action'
]
},
{
id: 'table-columns-disabled',
label: '',
sectionId: 'settings-sections-subnetNodes',
type: 'hidden',
defaultValue: []
}
] as const;
};
export { createNodesSettings };

View file

@ -1,20 +1,15 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import {nodesSettings} from "./config/nodesSettings"; import {createNodesSettings} from "./config/nodesSettings";
import { networkSettings } from './config/networkSettings'; import { createNetworkSettings } from './config/networkSettings';
import {appearanceSettings} from "./config/appearanceSettings"; import {createAppearanceSettings} from "./config/appearanceSettings";
type SettingValue = any; type SettingValue = any;
type SettingsDictionary = Record<string, SettingValue>; type SettingsDictionary = Record<string, SettingValue>;
interface SettingOption {
readonly value: any;
readonly label: string;
}
export type Setting = { export type Setting = {
readonly id: string; readonly id: string;
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button'; readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button' | 'hidden';
readonly label: string; readonly label: string;
readonly defaultValue: any; readonly defaultValue: any;
value?: string; value?: string;
@ -38,20 +33,16 @@ type SettingsState = {
resetSettings: () => void; resetSettings: () => void;
applyChanges: () => void; applyChanges: () => void;
discardChanges: () => void; discardChanges: () => void;
resetToDefaults: () => void;
getSettingValue: <T = any>(id: string) => T; getSettingValue: <T = any>(id: string) => T;
getNumberSetting: (id: string, fallback?: number) => number; getNumberSetting: (id: string, fallback?: number) => number;
updateSettingsConfig: () => void;
}; };
const defaultSettings: Setting[] = [
...nodesSettings,
...networkSettings,
...appearanceSettings,
];
export const useSettingsStore = create<SettingsState>()( export const useSettingsStore = create<SettingsState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
settingsConfig: defaultSettings, settingsConfig: [],
settingsValues: {}, settingsValues: {},
activeSection: null, activeSection: null,
pendingChanges: {}, pendingChanges: {},
@ -63,6 +54,16 @@ export const useSettingsStore = create<SettingsState>()(
pendingChanges: { ...state.pendingChanges, [id]: value } pendingChanges: { ...state.pendingChanges, [id]: value }
})), })),
resetToDefaults: () => set({
settingsValues: Object.fromEntries(
get().settingsConfig.map(setting => [
setting.id,
setting.defaultValue
])
),
pendingChanges: {}
}),
applyChanges: () => applyChanges: () =>
set((state) => ({ set((state) => ({
settingsValues: { ...state.settingsValues, ...state.pendingChanges }, settingsValues: { ...state.settingsValues, ...state.pendingChanges },
@ -84,6 +85,15 @@ export const useSettingsStore = create<SettingsState>()(
set({ settingsValues: newValues }); set({ settingsValues: newValues });
}, },
updateSettingsConfig: () => {
const defaultSettings: Setting[] = [
...createNodesSettings(),
...createNetworkSettings(),
...createAppearanceSettings(),
];
set({ settingsConfig: defaultSettings });
},
getSetting: (id) => { getSetting: (id) => {
const state = get(); const state = get();
const setting = state.settingsConfig.find(s => s.id === id); const setting = state.settingsConfig.find(s => s.id === id);

View file

@ -3,10 +3,10 @@ import { ThemeProvider as StyledTheme } from 'styled-components';
import { light, dark} from '../const/theme/theme'; import { light, dark} from '../const/theme/theme';
import { GlobalStyle } from '../const/theme/globalStyles'; import { GlobalStyle } from '../const/theme/globalStyles';
import { themeNames, ThemName } from '../const/theme/names'; import { themeNames, ThemName } from '../const/theme/names';
import {useThemeSettings} from "../hooks/useSettings";
type ThemeContextType = { type ThemeContextType = {
theme: ThemName; theme: ThemName;
toggleTheme: () => void;
}; };
const ThemeContext = createContext<ThemeContextType | undefined>(undefined); const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
@ -23,32 +23,13 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const getSystemTheme = () => const getSystemTheme = () =>
window.matchMedia('(prefers-color-scheme: dark)').matches ? themeNames[2] : themeNames[1]; window.matchMedia('(prefers-color-scheme: dark)').matches ? themeNames[2] : themeNames[1];
const getInitialTheme = () => { const getInitialTheme = useThemeSettings();
let savedTheme = localStorage.getItem('theme') as ThemName;
if (!savedTheme) {
savedTheme = themeNames[3]; // Устанавливаем значение по умолчанию
localStorage.setItem('theme', savedTheme); // Сохраняем без JSON.stringify
}
return savedTheme;
};
const [theme, setTheme] = useState<ThemName>(getInitialTheme); const [theme, setTheme] = useState<ThemName>(getInitialTheme);
const toggleTheme = () => { useEffect(() => {
setTheme((prev) => { setTheme(getInitialTheme)
let newTheme: ThemName; }, [getInitialTheme]);
if (prev === themeNames[3]) {
newTheme = themeNames[1];
} else if (prev === themeNames[1]) {
newTheme = themeNames[2];
} else {
newTheme = themeNames[3];
}
localStorage.setItem('theme', newTheme);
return newTheme;
});
};
useEffect(() => { useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
@ -68,8 +49,8 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const currentTheme = theme === themeNames[3] ? getSystemTheme() : theme; const currentTheme = theme === themeNames[3] ? getSystemTheme() : theme;
return ( return (
<ThemeContext.Provider value={{ theme, toggleTheme }}> <ThemeContext.Provider value={{ theme }}>
<StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark, toggleTheme }}> <StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark }}>
<GlobalStyle /> <GlobalStyle />
{children} {children}
</StyledTheme> </StyledTheme>

View file

@ -0,0 +1,11 @@
export const supportedLanguages = {
en: 'English',
ru: 'Русский',
} as const;
export type Language = keyof typeof supportedLanguages;
export const defaultLanguage = 'ru';
export const namespaces = ['common', 'components'] as const;
export type Namespace = (typeof namespaces)[number];

View file

@ -0,0 +1,11 @@
import { Namespace } from './config';
import { useI18n } from './provider';
export const useTranslation = (ns?: Namespace) => {
const { t, language } = useI18n();
return {
t: (key: string) => t(key, ns),
language,
};
};

View file

@ -0,0 +1,92 @@
'use client'
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { defaultLanguage, Language, Namespace, namespaces, supportedLanguages } from './config';
type Translation = Record<string, string>;
type NamespaceTranslations = Record<Namespace, Translation>;
type LanguageTranslations = Record<Language, NamespaceTranslations>;
const I18nContext = createContext<{
language: Language;
t: (key: string, namespace?: Namespace) => string;
}>({
language: defaultLanguage,
t: (key: string) => key,
});
interface I18nProviderProps {
children: React.ReactNode;
initialLanguage: Language;
}
export const I18nProvider = ({ children, initialLanguage }: I18nProviderProps) => {
const [language, setLanguage] = useState<Language>(initialLanguage);
const [translations, setTranslations] = useState<LanguageTranslations>({} as LanguageTranslations);
// Загрузка переводов
useEffect(() => {
const loadTranslations = async () => {
const newTranslations: LanguageTranslations = { ...translations };
await Promise.all(
(Object.keys(supportedLanguages) as Language[]).map(async (lang) => {
if (!newTranslations[lang]) {
newTranslations[lang] = {} as NamespaceTranslations;
await Promise.all(
namespaces.map(async (ns) => {
try {
const res = await fetch(`/locales/${lang}/${ns}.json`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
newTranslations[lang][ns] = data;
} catch (e) {
console.error(`Failed to load ${lang}/${ns} translations`, e);
newTranslations[lang][ns] = {};
}
})
);
}
})
);
setTranslations(newTranslations);
};
loadTranslations();
}, []);
useEffect(() => {
setLanguage(initialLanguage);
}, [initialLanguage]);
const t = useMemo(() => {
return (key: string, namespace: Namespace = 'common') => {
return translations[language]?.[namespace]?.[key] || key;
};
}, [language, translations]);
const contextValue = useMemo(() => ({
language,
t,
}), [language, t]);
return (
<I18nContext.Provider value={contextValue}>
{children}
</I18nContext.Provider>
);
};
export const useI18n = () => {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useI18n must be used within I18nProvider');
}
return context;
};

View file

@ -628,12 +628,12 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^20.9.0": "@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.7.7":
version "20.17.17" version "22.19.1"
resolved "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz"
integrity sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg== integrity sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==
dependencies: dependencies:
undici-types "~6.19.2" undici-types "~6.21.0"
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.13" version "15.7.13"
@ -743,12 +743,12 @@ author-regex@^1.0.0:
integrity sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g== integrity sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==
axios@^1.7.9: axios@^1.7.9:
version "1.8.4" version "1.13.2"
resolved "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz" resolved "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz"
integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==
dependencies: dependencies:
follow-redirects "^1.15.6" follow-redirects "^1.15.6"
form-data "^4.0.0" form-data "^4.0.4"
proxy-from-env "^1.1.0" proxy-from-env "^1.1.0"
balanced-match@^1.0.0: balanced-match@^1.0.0:
@ -772,9 +772,9 @@ boolean@^3.0.1:
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.12"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"
@ -817,6 +817,14 @@ cacheable-request@^7.0.2:
normalize-url "^6.0.1" normalize-url "^6.0.1"
responselike "^2.0.0" responselike "^2.0.0"
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
camelize@^1.0.0: camelize@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz" resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz"
@ -1032,6 +1040,15 @@ dir-compare@^3.0.0:
buffer-equal "^1.0.0" buffer-equal "^1.0.0"
minimatch "^3.0.4" minimatch "^3.0.4"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
electron-packager@^17.1.2: electron-packager@^17.1.2:
version "17.1.2" version "17.1.2"
resolved "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz" resolved "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz"
@ -1062,13 +1079,13 @@ electron-to-chromium@^1.5.73:
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.145.tgz" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.145.tgz"
integrity sha512-pZ5EcTWRq/055MvSBgoFEyKf2i4apwfoqJbK/ak2jnFq8oHjZ+vzc3AhRcz37Xn+ZJfL58R666FLJx0YOK9yTw== integrity sha512-pZ5EcTWRq/055MvSBgoFEyKf2i4apwfoqJbK/ak2jnFq8oHjZ+vzc3AhRcz37Xn+ZJfL58R666FLJx0YOK9yTw==
electron@^34.0.2: electron@^39.2.3:
version "34.0.2" version "39.2.3"
resolved "https://registry.npmjs.org/electron/-/electron-34.0.2.tgz" resolved "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz"
integrity sha512-u3F+DSUlg9NaGS+9qnYmSRN8VjAnc3LJDDk1ye1uISJnh4gjG76y3681qLowsPMx4obvCP2eBINnmbLo0yT5WA== integrity sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==
dependencies: dependencies:
"@electron/get" "^2.0.0" "@electron/get" "^2.0.0"
"@types/node" "^20.9.0" "@types/node" "^22.7.7"
extract-zip "^2.0.1" extract-zip "^2.0.1"
end-of-stream@^1.1.0: end-of-stream@^1.1.0:
@ -1090,7 +1107,7 @@ error-ex@^1.2.0:
dependencies: dependencies:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
es-define-property@^1.0.0: es-define-property@^1.0.0, es-define-property@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
@ -1100,6 +1117,23 @@ es-errors@^1.3.0:
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
es6-error@^4.1.1: es6-error@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz" resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz"
@ -1213,13 +1247,15 @@ follow-redirects@^1.15.6:
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
form-data@^4.0.0: form-data@^4.0.4:
version "4.0.1" version "4.0.5"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz" resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz"
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
dependencies: dependencies:
asynckit "^0.4.0" asynckit "^0.4.0"
combined-stream "^1.0.8" combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12" mime-types "^2.1.12"
fs-extra@^10.0.0: fs-extra@^10.0.0:
@ -1292,6 +1328,22 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-package-info@^1.0.0: get-package-info@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz" resolved "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz"
@ -1302,6 +1354,14 @@ get-package-info@^1.0.0:
lodash.get "^4.0.0" lodash.get "^4.0.0"
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-stream@^5.1.0: get-stream@^5.1.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
@ -1346,7 +1406,7 @@ globalthis@^1.0.1:
define-properties "^1.2.1" define-properties "^1.2.1"
gopd "^1.0.1" gopd "^1.0.1"
gopd@^1.0.1: gopd@^1.0.1, gopd@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
@ -1380,6 +1440,18 @@ has-property-descriptors@^1.0.0:
dependencies: dependencies:
es-define-property "^1.0.0" es-define-property "^1.0.0"
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hasown@^2.0.2: hasown@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
@ -1559,6 +1631,11 @@ matcher@^3.0.0:
dependencies: dependencies:
escape-string-regexp "^4.0.0" escape-string-regexp "^4.0.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mime-db@1.52.0: mime-db@1.52.0:
version "1.52.0" version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
@ -2120,10 +2197,10 @@ typescript@^4.9.5:
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
undici-types@~6.19.2: undici-types@~6.21.0:
version "6.19.8" version "6.21.0"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
universalify@^0.1.0: universalify@^0.1.0:
version "0.1.2" version "0.1.2"
@ -2162,9 +2239,9 @@ validate-npm-package-license@^3.0.1:
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
"vite@^4.2.0 || ^5.0.0 || ^6.0.0", vite@^6.0.0, vite@^6.3.4: "vite@^4.2.0 || ^5.0.0 || ^6.0.0", vite@^6.0.0, vite@^6.3.4:
version "6.3.4" version "6.4.1"
resolved "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz" resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
integrity sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw== integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
dependencies: dependencies:
esbuild "^0.25.0" esbuild "^0.25.0"
fdir "^6.4.4" fdir "^6.4.4"

3
SRV/.gitignore vendored
View file

@ -6,4 +6,5 @@ __pycache__
.wheels .wheels
.vendor .vendor
.python3-deb .python3-deb
storage/devices/ storage/subnets/
storage/wifi/

View file

@ -1,3 +1,27 @@
"""
Маршрутизация API-версии v1 для GONETS.
Модуль агрегирует и монтирует роутеры отдельных доменных областей под общим
корневым роутером. Каждый подключенный модуль предоставляет набор эндпоинтов
со своим префиксом и тегами для группировки в документации FastAPI.
Структура маршрутов:
- `/ufw` (UFW): управление правилами и состоянием Uncomplicated Firewall.
- `/ip` (IP): проверки и применение IP-конфигураций для сетевых профилей.
- `/network` (Network): получение и визуализация состояния интерфейсов,
управление позициями UI-контейнеров.
- `/auth` (Auth): регистрация, аутентификация (JWT), сессии.
- `/subnet` (Subnet): сканирование подсетей, заметки к устройствам,
взаимодействие с Wi-Fi (скан/подключение/отключение/статус).
- `WebSocket`: обработчики взаимодействия по WS (без префикса, путь внутри
модуля), периодическая трансляция сетевых данных.
- `/settings` (Settings): сервисные операции над локальными JSON-хранилищами
(очистка данных подсетей/wi-fi и пр.).
Назначение:
- Централизованная точка подключения доменных роутеров к приложению.
- Единообразие тегов и префиксов для автогенерации схемы и документации.
"""
from fastapi import APIRouter from fastapi import APIRouter
from .v1 import ufw, websocket, ip, network, auth, subnet, settings from .v1 import ufw, websocket, ip, network, auth, subnet, settings

View file

@ -3,6 +3,8 @@ from fastapi.security import OAuth2PasswordBearer
from services.auth_service import AuthService from services.auth_service import AuthService
from models.auth import UserCreate, UserLogin, UserResponse, Token from models.auth import UserCreate, UserLogin, UserResponse, Token
from storage.json_storage import JsonStorage from storage.json_storage import JsonStorage
from utils.i18n import get_translations, _
from babel.support import Translations
import logging import logging
import os import os
@ -13,9 +15,8 @@ router = APIRouter()
auth_service = AuthService() auth_service = AuthService()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@router.post("/register", response_model=UserResponse) @router.post("/register", response_model=UserResponse)
async def register(user: UserCreate): async def register(user: UserCreate, translations: Translations = Depends(get_translations)):
"""Регистрирует нового пользователя. """Регистрирует нового пользователя.
Создаёт нового пользователя на основе предоставленных данных и возвращает Создаёт нового пользователя на основе предоставленных данных и возвращает
@ -23,6 +24,7 @@ async def register(user: UserCreate):
Args: Args:
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin). user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
translations: Объект переводов для текущего языка.
Returns: Returns:
UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin). UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin).
@ -38,12 +40,11 @@ async def register(user: UserCreate):
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}") logger.error(f"Error in register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя") raise HTTPException(status_code=500, detail=translations.gettext("Не удалось зарегистрировать пользователя"))
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login(user: UserLogin, request: Request): async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations)):
"""Авторизует пользователя и создаёт сессию. """Авторизует пользователя и создаёт сессию.
Проверяет учетные данные пользователя и возвращает токен и session_id для Проверяет учетные данные пользователя и возвращает токен и session_id для
@ -52,6 +53,7 @@ async def login(user: UserLogin, request: Request):
Args: Args:
user (UserLogin): Объект с данными для входа (username, password). user (UserLogin): Объект с данными для входа (username, password).
request (Request): Объект запроса для получения client_id (IP-адрес клиента). request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns: Returns:
Token: Объект с access_token и session_id для авторизации. Token: Объект с access_token и session_id для авторизации.
@ -62,21 +64,18 @@ async def login(user: UserLogin, request: Request):
""" """
try: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Попытка авторизации для client_id {client_id}") logger.debug(f"Попытка входа в систему клиента {client_id}")
result = auth_service.login(user, client_id) result = auth_service.login(user, client_id)
logger.info( logger.info(f"Успешная авторизация для клиента {client_id}, Идентификатор сессии: {result['session_id']}")
f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}"
)
return result return result
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка при авторизации: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя") raise HTTPException(status_code=500, detail=translations.gettext("Не удалось авторизовать пользователя"))
@router.get("/me") @router.get("/me")
async def get_current_user(request: Request): async def get_current_user(request: Request, translations: Translations = Depends(get_translations)):
"""Возвращает данные текущего пользователя и токен. """Возвращает данные текущего пользователя и токен.
Проверяет активную сессию по client_id и возвращает информацию о пользователе Проверяет активную сессию по client_id и возвращает информацию о пользователе
@ -84,6 +83,7 @@ async def get_current_user(request: Request):
Args: Args:
request (Request): Объект запроса для получения client_id (IP-адрес клиента). request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и
@ -95,29 +95,28 @@ async def get_current_user(request: Request):
""" """
try: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Запрос /me для client_id {client_id}") logger.debug(f"Запрос /me для клиента {client_id}")
result = auth_service.get_user_by_session(client_id) result = auth_service.get_user_by_session(client_id)
return { return {
"user": result["user"], "user": result["user"],
"access_token": result["access_token"], "access_token": result["access_token"],
"token_type": "bearer", "token_type": translations.gettext("bearer"),
} }
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error( logger.error(f"Ошибка при get_current_user: {type(e).__name__} - {str(e)}")
f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить данные пользователя"))
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
@router.get("/logout") @router.get("/logout")
async def logout(request: Request): async def logout(request: Request, translations: Translations = Depends(get_translations)):
"""Выход из системы, удаление сессии. """Выход из системы, удаление сессии.
Удаляет активную сессию пользователя по client_id. Удаляет активную сессию пользователя по client_id.
Args: Args:
request (Request): Объект запроса для получения client_id (IP-адрес клиента). request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Пустой словарь или сообщение об успешном выходе. dict: Пустой словарь или сообщение об успешном выходе.
@ -128,17 +127,16 @@ async def logout(request: Request):
""" """
try: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Запрос logout для client_id {client_id}") logger.debug(f"Запрос на выход из системы для клиента {client_id}")
return auth_service.logout(client_id) return auth_service.logout(client_id)
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}") logger.error(f"Не удалось выйти из системы: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось выйти из системы") raise HTTPException(status_code=500, detail=translations.gettext("Не удалось выйти из системы"))
@router.get("/check_json") @router.get("/check_json")
async def check_users_file(): async def check_users_file(translations: Translations = Depends(get_translations)):
"""Проверяет, существует ли файл users.json и содержит ли он пользователей. """Проверяет, существует ли файл users.json и содержит ли он пользователей.
Возвращает информацию о состоянии файла и наличии пользователей. Возвращает информацию о состоянии файла и наличии пользователей.
@ -161,31 +159,25 @@ async def check_users_file():
return { return {
"exists": False, "exists": False,
"has_users": False, "has_users": False,
"message": "Файл users.json не существует", "message": translations.gettext("Файл users.json не существует"),
} }
data = storage._read_data() data = storage._read_data()
has_users = len(data.get("users", [])) > 0 has_users = len(data.get("users", [])) > 0
if has_users: if has_users:
logger.info( logger.info(f"Файл {file_path} существует и содержит пользователей")
f"Файл {file_path} существует и содержит пользователей"
)
return { return {
"exists": True, "exists": True,
"has_users": True, "has_users": True,
"message": "Файл users.json существует и содержит пользователей", "message": translations.gettext("Файл users.json существует и содержит пользователей"),
} }
else: else:
logger.info( logger.info(f"Файл {file_path} существует, но не содержит пользователей")
f"Файл {file_path} существует, но не содержит пользователей"
)
return { return {
"exists": True, "exists": True,
"has_users": False, "has_users": False,
"message": "Файл users.json существует, но не содержит пользователей", "message": translations.gettext("Файл users.json существует, но не содержит пользователей"),
} }
except Exception as e: except Exception as e:
logger.error( logger.error(f"Ошибка при проверке файла: {type(e).__name__} - {str(e)}")
f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}" raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при проверке файла"))
)
raise HTTPException(status_code=500, detail="Ошибка при проверке файла")

View file

@ -1,13 +1,14 @@
from typing import Dict, Any from typing import Dict, Any
from fastapi import APIRouter, Body, HTTPException from fastapi import APIRouter, Body, HTTPException, Depends
from services.ip_service import IPService from services.ip_service import IPService
from utils.i18n import get_translations
from babel.support import Translations
router = APIRouter() router = APIRouter()
ip_service = IPService() ip_service = IPService()
@router.post("/check_ip") @router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...)): async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Проверяет доступность IP-адреса перед его применением. """Проверяет доступность IP-адреса перед его применением.
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес, Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
@ -16,6 +17,7 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
Args: Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая data (Dict[str, Any]): Словарь с данными конфигурации, включая
"ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса. "ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса.
translations: Объект переводов для текущего языка.
Returns: Returns:
Dict[str, Any]: Словарь с ключами: Dict[str, Any]: Словарь с ключами:
@ -26,16 +28,16 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
HTTPException: Если произошла внутренняя ошибка (status_code=500). HTTPException: Если произошла внутренняя ошибка (status_code=500).
""" """
try: try:
result = await ip_service.check_ip_config(data) result = await ip_service.check_ip_config(data, translations)
return result return result
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail=f"Ошибка проверки IP конфигурации: {str(e)}") detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
@router.post("/apply_ip") @router.post("/apply_ip")
async def apply_ip_config(data: Dict[str, Any] = Body(...)): async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Применяет проверенные IP-настройки к указанному сетевому профилю. """Применяет проверенные IP-настройки к указанному сетевому профилю.
Обновляет настройки соединения через NetworkManager, отключает и Обновляет настройки соединения через NetworkManager, отключает и
@ -44,6 +46,7 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...)):
Args: Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек. "connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с ключами: dict: Словарь с ключами:
@ -55,19 +58,23 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...)):
произошла внутренняя ошибка (status_code=500). произошла внутренняя ошибка (status_code=500).
""" """
try: try:
await ip_service.apply_ip_config(data) await ip_service.apply_ip_config(data, translations)
return { return {
"success": True, "success": True,
"message": f"Настройки сетевого профиля для {data['connection_id']} применены", "message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
} }
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=translations.gettext(str(e)))
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}") raise HTTPException(
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
@router.post("/save_ip") @router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...)): async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)):
"""Применяет проверенные IP-настройки для отката сетевого профиля. """Применяет проверенные IP-настройки для отката сетевого профиля.
Используется для сохранения и отката настроек сетевого профиля к предыдущему Используется для сохранения и отката настроек сетевого профиля к предыдущему
@ -76,6 +83,7 @@ async def save_ip_config(data: Dict[str, Any] = Body(...)):
Args: Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек. "connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с ключами: dict: Словарь с ключами:
@ -87,12 +95,17 @@ async def save_ip_config(data: Dict[str, Any] = Body(...)):
произошла внутренняя ошибка (status_code=500). произошла внутренняя ошибка (status_code=500).
""" """
try: try:
await ip_service.apply_ip_config(data) await ip_service.apply_ip_config(data, translations)
return { return {
"success": True, "success": True,
"message": f"Настройки сетевого профиля для {data['connection_id']} применены", "message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
} }
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=translations.gettext(str(e)))
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}") raise HTTPException(
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)

View file

@ -1,7 +1,6 @@
from fastapi import APIRouter, Body, HTTPException from fastapi import APIRouter, Body, HTTPException, Depends
import logging import logging
from typing import Dict, Tuple, List, Union from typing import Dict, Tuple, List, Union
from services.network_service import NetworkService from services.network_service import NetworkService
from models.network import ( from models.network import (
NetworkInterfacesContainer, NetworkInterfacesContainer,
@ -10,6 +9,8 @@ from models.network import (
NetworkInterface, NetworkInterface,
WiFiDevice, WiFiDevice,
) )
from utils.i18n import get_translations, _, _load_translations
from babel.support import Translations
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,7 +18,6 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
network_service = NetworkService() network_service = NetworkService()
class ContainerPositionManager: class ContainerPositionManager:
"""Класс для управления позициями и состоянием контейнеров. """Класс для управления позициями и состоянием контейнеров.
@ -44,11 +44,12 @@ class ContainerPositionManager:
}, },
} }
def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]: def get_position_and_state(self, container_name: str, translations: Translations) -> Tuple[int, int, bool]:
"""Получает текущие координаты и состояние указанного контейнера. """Получает текущие координаты и состояние указанного контейнера.
Args: Args:
container_name (str): Имя контейнера ("activation" или "deactivation"). container_name (str): Имя контейнера ("activation" или "deactivation").
translations: Объект переводов для текущего языка.
Returns: Returns:
Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen). Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen).
@ -58,7 +59,7 @@ class ContainerPositionManager:
""" """
container = self.positions.get(container_name) container = self.positions.get(container_name)
if not container: if not container:
raise ValueError(f"Неверное имя контейнера: {container_name}") raise ValueError(translations.gettext("Неверное имя контейнера: {container_name}").format(container_name=container_name))
return container["x"], container["y"], container["isOpen"] return container["x"], container["y"], container["isOpen"]
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None: def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
@ -76,17 +77,18 @@ class ContainerPositionManager:
container_name = "activation" if container_id == 1 else "deactivation" container_name = "activation" if container_id == 1 else "deactivation"
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen} self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
container_manager = ContainerPositionManager() container_manager = ContainerPositionManager()
async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]:
async def get_network_data() -> Dict[str, Dict]:
"""Возвращает текущие сетевые данные в виде словаря. """Возвращает текущие сетевые данные в виде словаря.
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные, Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные,
и возвращает их в формате Activation и Deactivation с учётом позиций и состояний и возвращает их в формате Activation и Deactivation с учётом позиций и состояний
контейнеров. контейнеров.
Args:
translations: Объект переводов для текущего языка.
Returns: Returns:
Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими
данные о соответствующих контейнерах. данные о соответствующих контейнерах.
@ -94,14 +96,14 @@ async def get_network_data() -> Dict[str, Dict]:
Raises: Raises:
HTTPException: Если произошла ошибка при получении данных (status_code=500). HTTPException: Если произошла ошибка при получении данных (status_code=500).
""" """
logger.debug("Вызов get_network_data()") logger.debug("Calling get_network_data()")
try: try:
network_data = await network_service.get_network_interfaces_container() network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces) activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation")) activation_x, activation_y, activation_is_open = container_manager.get_position_and_state("activation", translations)
deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation")) deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state("deactivation", translations)
activation = Activation( activation = Activation(
id=1, id=1,
@ -123,9 +125,8 @@ async def get_network_data() -> Dict[str, Dict]:
"deactivation": deactivation.model_dump(), "deactivation": deactivation.model_dump(),
} }
except Exception as e: except Exception as e:
logger.error(f"Ошибка в get_network_data: {e}") logger.error(f"Error in get_network_data: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные") raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить сетевые данные"))
def _split_interfaces( def _split_interfaces(
interfaces: List[Union[NetworkInterface, WiFiDevice]], interfaces: List[Union[NetworkInterface, WiFiDevice]],
@ -148,9 +149,8 @@ def _split_interfaces(
deactivation_interfaces.append(interface) deactivation_interfaces.append(interface)
return activation_interfaces, deactivation_interfaces return activation_interfaces, deactivation_interfaces
@router.post("/update_container_position") @router.post("/update_container_position")
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)): async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = Depends(get_translations)):
"""Обновляет позицию и состояние контейнера. """Обновляет позицию и состояние контейнера.
Принимает данные о новом положении и состоянии контейнера и обновляет их Принимает данные о новом положении и состоянии контейнера и обновляет их
@ -162,6 +162,7 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
- "container_x" (int): Новая координата X. - "container_x" (int): Новая координата X.
- "container_y" (int): Новая координата Y. - "container_y" (int): Новая координата Y.
- "isOpen" (bool): Новое состояние контейнера (открыт/закрыт). - "isOpen" (bool): Новое состояние контейнера (открыт/закрыт).
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с ключами: dict: Словарь с ключами:
@ -178,7 +179,7 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
] ]
if not all(key in data for key in required_keys): if not all(key in data for key in required_keys):
raise ValueError( raise ValueError(
"Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen" translations.gettext("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
) )
container_id = data["container_id"] container_id = data["container_id"]
@ -190,24 +191,26 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
or not isinstance(container_x, int) or not isinstance(container_x, int)
or not isinstance(container_y, int) or not isinstance(container_y, int)
or not isinstance(is_open, bool)): or not isinstance(is_open, bool)):
raise ValueError("Неверный тип данных в запросе") raise ValueError(translations.gettext("Неверный тип данных в запросе"))
container_manager.update_position(container_id, container_x, container_y, is_open) container_manager.update_position(container_id, container_x, container_y, is_open)
return { return {
"success": True, "success": True,
"message": "Позиция и состояние контейнера обновлены" "message": translations.gettext("Позиция и состояние контейнера обновлены")
} }
except Exception as e: except Exception as e:
logger.error(f"Ошибка обновления позиции контейнера: {e}") logger.error(f"Error updating container position: {e}")
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}") raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e)))
@router.get("/get_hostname") @router.get("/get_hostname")
async def get_hostname(): async def get_hostname(translations: Translations = Depends(get_translations)):
"""Возвращает имя хоста системы. """Возвращает имя хоста системы.
Выполняет запрос к сервису для получения имени хоста. Выполняет запрос к сервису для получения имени хоста.
Args:
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с ключом "hostname" и значением имени хоста. dict: Словарь с ключом "hostname" и значением имени хоста.
@ -218,5 +221,5 @@ async def get_hostname():
hostname_info = await network_service.get_hostname() hostname_info = await network_service.get_hostname()
return hostname_info return hostname_info
except Exception as e: except Exception as e:
logger.error(f"Ошибка в get_hostname: {e}") logger.error(f"Error in get_hostname: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить имя хоста") raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить имя хоста"))

View file

@ -1,15 +1,31 @@
from fastapi import APIRouter, HTTPException import os
import logging
import subprocess
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Response, Request, Body
from starlette.responses import FileResponse
from services.settings_service import SettingsService from services.settings_service import SettingsService
from utils.i18n import get_translations
from babel.support import Translations
from models.subnet import SubnetScanRequest # Предполагается, что модель определена
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
settings_service = SettingsService() settings_service = SettingsService()
@router.post("/clear_subnet") @router.post("/clear_subnet")
def clear_subnet(): def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети, сохранённые в devices.json. """Очищает данные узлов подсети, сохранённые в devices.json.
Удаляет все записи об узлах подсети, оставляя файл пустым. Удаляет все записи об узлах подсети, оставляя файл пустым.
Args:
subnet (Optional[str]): Подсеть в формате CIDR (например, "10.1.1.0/24").
Если None, очищает все подсети.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешной очистке. dict: Словарь с сообщением об успешной очистке.
@ -17,18 +33,27 @@ def clear_subnet():
HTTPException: Если произошла ошибка очистки (status_code=500). HTTPException: Если произошла ошибка очистки (status_code=500).
""" """
try: try:
success = settings_service.clear_subnet() success = settings_service.clear_subnet(subnet)
if success: if success:
return {"message": "Данные узлов подсети успешно очищены"} if subnet:
return {"message": translations.gettext("Данные узлов подсети {subnet} успешно очищены").format(subnet=subnet)}
else:
return {"message": translations.gettext("Данные всех подсетей успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные узлов подсети"))
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}") logger.error(f"Error clearing subnet data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_wifi") @router.post("/clear_wifi")
def clear_wifi(): def clear_wifi(translations: Translations = Depends(get_translations)):
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json. """Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
Удаляет все записи о точках доступа, оставляя файл пустым. Удаляет все записи о точках доступа, оставляя файл пустым.
Args:
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешной очистке. dict: Словарь с сообщением об успешной очистке.
@ -38,16 +63,22 @@ def clear_wifi():
try: try:
success = settings_service.clear_wifi() success = settings_service.clear_wifi()
if success: if success:
return {"message": "Данные точек доступа Wi-Fi успешно очищены"} return {"message": translations.gettext("Данные точек доступа Wi-Fi успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные точек доступа Wi-Fi"))
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}") logger.error(f"Error clearing wifi data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_all_data") @router.post("/clear_all_data")
def clear_all_data(): def clear_all_data(translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах. """Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
Удаляет все записи из devices.json и wifi.json. Удаляет все записи из devices.json и wifi.json.
Args:
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешной очистке. dict: Словарь с сообщением об успешной очистке.
@ -57,6 +88,122 @@ def clear_all_data():
try: try:
success = settings_service.clear_all_data() success = settings_service.clear_all_data()
if success: if success:
return {"message": "Все сетевые данные успешно очищены"} return {"message": translations.gettext("Все сетевые данные успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить все сетевые данные"))
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}") logger.error(f"Error clearing all data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
WIFI_PATH = "storage/wifi/wifi.json"
@router.post("/export_devices", response_class=FileResponse)
async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON для указанной подсети.
Args:
request (SubnetScanRequest): Объект с полем subnet в формате CIDR.
translations: Объект переводов для текущего языка.
Returns:
FileResponse: Файл <subnet>.json для скачивания.
Raises:
HTTPException: Если файл не найден (status_code=404).
"""
subnet = request.subnet
file_path = f"storage/subnets/{subnet.replace('/', '_')}.json"
if not os.path.exists(file_path):
logger.warning(f"Export file not found: {file_path}")
raise HTTPException(status_code=404, detail=translations.gettext("Файл для подсети {subnet} не найден").format(subnet=subnet))
logger.info(f"Exporting file: {file_path}")
return FileResponse(
path=file_path,
filename=f"{subnet.replace('/', '_')}.json",
media_type="application/json"
)
@router.get("/export_wifi", response_class=FileResponse)
async def export_wifi(translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON.
Args:
translations: Объект переводов для текущего языка.
Returns:
FileResponse: Файл wifi.json для скачивания.
Raises:
HTTPException: Если файл не найден (status_code=404).
"""
if not os.path.exists(WIFI_PATH):
logger.warning(f"Export file not found: {WIFI_PATH}")
raise HTTPException(status_code=404, detail=translations.gettext("Файл не найден"))
logger.info(f"Exporting file: {WIFI_PATH}")
return FileResponse(
path=WIFI_PATH,
filename="wifi.json",
media_type="application/json"
)
@router.post("/set_language")
def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations)):
"""
Устанавливает куку locale для смены языка пользователя.
Args:
locale (str): Используемый язык.
response (Response): Запрос.
translations: Объект переводов текущего языка.
Returns:
dict: Сообщение об успешной установке языка.
"""
lc_messages_path = os.path.join(os.path.dirname(__file__), '..', '..', 'locales', locale, 'LC_MESSAGES')
if not os.path.isdir(lc_messages_path):
raise HTTPException(
status_code=400,
detail=translations.gettext("Язык '{locale}' не поддерживается.").format(locale=locale)
)
has_mo_or_po = any(f.endswith('.mo') or f.endswith('.po') for f in os.listdir(lc_messages_path))
if not has_mo_or_po:
raise HTTPException(
status_code=400,
detail=translations.gettext("Язык '{locale}' не поддерживается (нет файлов переводов).").format(locale=locale)
)
response.set_cookie(key="locale", value=locale, path="/", httponly=True)
return {"message": translations.gettext("Язык успешно установлен: {locale}").format(locale=locale)}
@router.get("/get_system_language")
async def get_system_language(request: Request, translations: Translations = Depends(get_translations)):
"""Выводит текущий язык, установленный в системе на основе locale.
Выполняет команду 'locale' в системе и извлекает параметры LANG
Args:
request (Request): Объект запроса для доступа к параметрам.
translations: Объект переводов текущего языка.
Returns:
dict: Словарь с текущим языком системы.
Raises:
HTTPException: Если не удалось извлечь системный язык (status_code=500).
"""
try:
result = subprocess.run(['locale'], capture_output=True, text=True, check=True)
output = result.stdout
lang = next((line.split('=')[1].strip() for line in output.splitlines() if line.startswith('LANG=')), None)
if lang:
return {"language": lang}
else:
logger.warning(f"No locale found: {output}")
return {"language": 'unknown'}
except subprocess.CalledProcessError as e:
logger.error(f"Error executing locale command: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось определить язык в системе"))
except Exception as e:
logger.error(f"Unexpected error getting system language: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при определении языка в системе {error}").format(error=str(e)))

View file

@ -1,25 +1,29 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException, Body, Depends
from typing import List, Dict from models.subnet import SubnetScanResult, SubnetScanRequest
from models.subnet import SubnetScanResult
from models.wifi import WifiScanResult from models.wifi import WifiScanResult
from models.request import NoteRequest, WifiConnectRequest, WifiDisconnectRequest from models.request import NoteRequest, WifiConnectRequest, WifiDisconnectRequest
from services.subnet_service import SubnetService from services.subnet_service import SubnetService, get_subnet_service
from services.wifi_service import WifiService from services.wifi_service import WifiService
from services.connect_service import WifiConnectionService from services.connect_service import WifiConnectionService
from utils.i18n import get_translations, _
from babel.support import Translations
import logging
router = APIRouter() router = APIRouter()
@router.get("/devices", response_model=SubnetScanResult) logging.basicConfig(level=logging.INFO)
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None): logger = logging.getLogger(__name__)
@router.post("/devices", response_model=SubnetScanResult)
def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Сканирует локальную подсеть и возвращает данные об устройствах. """Сканирует локальную подсеть и возвращает данные об устройствах.
Выполняет сканирование указанной подсети для обнаружения активных устройств Выполняет сканирование указанной подсети для обнаружения активных устройств.
с возможностью проверки указанных портов.
Args: Args:
subnet (str, optional): Адрес подсети в формате CIDR (например, "192.168.1.0/24"). request (SubnetScanRequest): Объект с полем subnet.
По умолчанию "192.168.1.0/24". subnet_service: Сервис сканирования подсети.
ports (List[int], optional): Список портов для сканирования. По умолчанию None. translations: Объект переводов для текущего языка.
Returns: Returns:
SubnetScanResult: Объект с результатами сканирования (устройства и их данные). SubnetScanResult: Объект с результатами сканирования (устройства и их данные).
@ -28,20 +32,26 @@ def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
HTTPException: Если произошла ошибка сканирования (status_code=500). HTTPException: Если произошла ошибка сканирования (status_code=500).
""" """
try: try:
subnet_service = SubnetService() # Если subnet пустая строка, определяем автоматически
result = subnet_service.scan_subnet(subnet, ports) subnet = request.subnet.strip() if request.subnet else None
logger.info(f"Requested subnet scan for: {subnet or 'auto-detected'}")
result = subnet_service.scan_subnet(subnet)
if not result.devices and result.error:
logger.warning(f"No devices found in subnet {subnet or 'auto-detected'}")
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка сканирования: {str(e)}") logger.error(f"Error in scan_subnet: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования подсети: {error}").format(error=str(e)))
@router.get("/wifi", response_model=WifiScanResult) @router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30): def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них. """Сканирует Wi-Fi точки доступа и возвращает данные о них.
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом. Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
Args: Args:
timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30. timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30.
translations: Объект переводов для текущего языка.
Returns: Returns:
WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей). WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей).
@ -54,10 +64,10 @@ def scan_wifi(timeout: int = 30):
result = wifi_service.scan_wifi(timeout=timeout) result = wifi_service.scan_wifi(timeout=timeout)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка сканирования Wi-Fi: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования Wi-Fi: {error}").format(error=str(e)))
@router.post("/devices/{mac}/note") @router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest): def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Добавляет заметку для устройства по MAC-адресу. """Добавляет заметку для устройства по MAC-адресу.
Привязывает заметку к устройству, идентифицируемому по MAC-адресу. Привязывает заметку к устройству, идентифицируемому по MAC-адресу.
@ -65,6 +75,8 @@ def add_note(mac: str, request: NoteRequest):
Args: Args:
mac (str): MAC-адрес устройства. mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для текста заметки. request (NoteRequest): Объект с полем "note" для текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном добавлении заметки. dict: Словарь с сообщением об успешном добавлении заметки.
@ -74,21 +86,22 @@ def add_note(mac: str, request: NoteRequest):
существует (status_code=400), или произошла внутренняя ошибка (status_code=500). существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
""" """
try: try:
subnet_service = SubnetService() success, result = subnet_service.add_note(mac, request.note, translations)
success, error_message = subnet_service.add_note(mac, request.note)
if not success: if not success:
if "не найдено" in error_message: if result[0] == "not_found":
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif "уже существует" in error_message: elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} уже существует").format(mac=mac))
return {"message": f"Заметка '{request.note}' добавлена для устройства {mac}"} else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' добавлена для устройства {mac}").format(note=request.note, mac=mac)}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/devices/{mac}/note") @router.put("/devices/{mac}/note")
def update_note(mac: str, request: NoteRequest): def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Обновляет заметку для устройства по MAC-адресу. """Обновляет заметку для устройства по MAC-адресу.
Изменяет существующую заметку, привязанную к устройству. Изменяет существующую заметку, привязанную к устройству.
@ -96,6 +109,8 @@ def update_note(mac: str, request: NoteRequest):
Args: Args:
mac (str): MAC-адрес устройства. mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для нового текста заметки. request (NoteRequest): Объект с полем "note" для нового текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном обновлении заметки. dict: Словарь с сообщением об успешном обновлении заметки.
@ -105,27 +120,30 @@ def update_note(mac: str, request: NoteRequest):
существует (status_code=400), или произошла внутренняя ошибка (status_code=500). существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
""" """
try: try:
subnet_service = SubnetService() success, result = subnet_service.update_note(mac, request.note, translations)
success, error_message = subnet_service.update_note(mac, request.note)
if not success: if not success:
if "не найдено" in error_message: if result[0] == "not_found":
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif "не существует" in error_message: elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
return {"message": f"Заметка '{request.note}' обновлена для устройства {mac}"} else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' обновлена для устройства {mac}").format(note=request.note, mac=mac)}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/devices/{mac}/note") @router.delete("/devices/{mac}/note")
def delete_note(mac: str): def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Удаляет заметку для устройства по MAC-адресу. """Удаляет заметку для устройства по MAC-адресу.
Удаляет привязанную заметку для указанного устройства. Удаляет привязанную заметку для указанного устройства.
Args: Args:
mac (str): MAC-адрес устройства. mac (str): MAC-адрес устройства.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном удалении заметки. dict: Словарь с сообщением об успешном удалении заметки.
@ -135,21 +153,22 @@ def delete_note(mac: str):
существует (status_code=400), или произошла внутренняя ошибка (status_code=500). существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
""" """
try: try:
subnet_service = SubnetService() success, result = subnet_service.delete_note(mac, translations)
success, error_message = subnet_service.delete_note(mac)
if not success: if not success:
if "не найдено" in error_message: if result[0] == "not_found":
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif "не существует" in error_message: elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
return {"message": f"Заметка удалена для устройства {mac}"} else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка удалена для устройства {mac}").format(mac=mac)}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/{ssid}/note") @router.post("/wifi/{ssid}/note")
def add_wifi_note(ssid: str, request: NoteRequest): def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Добавляет заметку для сети Wi-Fi по SSID. """Добавляет заметку для сети Wi-Fi по SSID.
Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID. Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID.
@ -157,6 +176,7 @@ def add_wifi_note(ssid: str, request: NoteRequest):
Args: Args:
ssid (str): Идентификатор Wi-Fi сети. ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для текста заметки. request (NoteRequest): Объект с полем "note" для текста заметки.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном добавлении заметки. dict: Словарь с сообщением об успешном добавлении заметки.
@ -167,20 +187,22 @@ def add_wifi_note(ssid: str, request: NoteRequest):
""" """
try: try:
wifi_service = WifiService() wifi_service = WifiService()
success, error_message = wifi_service.add_note(ssid, request.note) success, result = wifi_service.add_note(ssid, request.note)
if not success: if not success:
if "не найдена" in error_message: if result[0] == "not_found":
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif "уже существует" in error_message: elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} уже существует").format(ssid=ssid))
return {"message": f"Заметка '{request.note}' добавлена для сети с SSID {ssid}"} else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' добавлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/wifi/{ssid}/note") @router.put("/wifi/{ssid}/note")
def update_wifi_note(ssid: str, request: NoteRequest): def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Обновляет заметку для сети Wi-Fi по SSID. """Обновляет заметку для сети Wi-Fi по SSID.
Изменяет существующую заметку, привязанную к Wi-Fi сети. Изменяет существующую заметку, привязанную к Wi-Fi сети.
@ -188,6 +210,7 @@ def update_wifi_note(ssid: str, request: NoteRequest):
Args: Args:
ssid (str): Идентификатор Wi-Fi сети. ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для нового текста заметки. request (NoteRequest): Объект с полем "note" для нового текста заметки.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном обновлении заметки. dict: Словарь с сообщением об успешном обновлении заметки.
@ -198,26 +221,29 @@ def update_wifi_note(ssid: str, request: NoteRequest):
""" """
try: try:
wifi_service = WifiService() wifi_service = WifiService()
success, error_message = wifi_service.update_note(ssid, request.note) success, result = wifi_service.update_note(ssid, request.note)
if not success: if not success:
if "не найдена" in error_message: if result[0] == "not_found":
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif "не существует" in error_message: elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
return {"message": f"Заметка '{request.note}' обновлена для сети с SSID {ssid}"} else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' обновлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/wifi/{ssid}/note") @router.delete("/wifi/{ssid}/note")
def delete_wifi_note(ssid: str): def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)):
"""Удаляет заметку для сети Wi-Fi по SSID. """Удаляет заметку для сети Wi-Fi по SSID.
Удаляет привязанную заметку для указанной Wi-Fi сети. Удаляет привязанную заметку для указанной Wi-Fi сети.
Args: Args:
ssid (str): Идентификатор Wi-Fi сети. ssid (str): Идентификатор Wi-Fi сети.
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном удалении заметки. dict: Словарь с сообщением об успешном удалении заметки.
@ -228,20 +254,22 @@ def delete_wifi_note(ssid: str):
""" """
try: try:
wifi_service = WifiService() wifi_service = WifiService()
success, error_message = wifi_service.delete_note(ssid) success, result = wifi_service.delete_note(ssid)
if not success: if not success:
if "не найдена" in error_message: if result[0] == "not_found":
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif "не существует" in error_message: elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
return {"message": f"Заметка удалена для сети с SSID {ssid}"} else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка удалена для сети с SSID {ssid}").format(ssid=ssid)}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}") raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/connect") @router.post("/wifi/connect")
def connect_to_wifi(request: WifiConnectRequest): def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)):
"""Подключается к Wi-Fi сети по SSID и паролю. """Подключается к Wi-Fi сети по SSID и паролю.
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
@ -249,6 +277,7 @@ def connect_to_wifi(request: WifiConnectRequest):
Args: Args:
request (WifiConnectRequest): Объект с полями "ssid" и "password". request (WifiConnectRequest): Объект с полями "ssid" и "password".
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном подключении. dict: Словарь с сообщением об успешном подключении.
@ -256,20 +285,26 @@ def connect_to_wifi(request: WifiConnectRequest):
Raises: Raises:
HTTPException: Если подключение не удалось (status_code=400). HTTPException: Если подключение не удалось (status_code=400).
""" """
wifi_service = WifiConnectionService() try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.connect(request.ssid, request.password) success, message = wifi_service.connect(request.ssid, request.password)
if not success: if not success:
raise HTTPException(status_code=400, detail=message) raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"message": f"Успешно подключено к {request.ssid}"} return {"message": translations.gettext("Успешно подключено к {ssid}").format(ssid=request.ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка подключения к Wi-Fi: {error}").format(error=str(e)))
@router.post("/wifi/disconnect") @router.post("/wifi/disconnect")
def disconnect_from_wifi(request: WifiDisconnectRequest): def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)):
"""Отключается от Wi-Fi сети по указанному SSID. """Отключается от Wi-Fi сети по указанному SSID.
Выполняет отключение от указанной Wi-Fi сети. Выполняет отключение от указанной Wi-Fi сети.
Args: Args:
request (WifiDisconnectRequest): Объект с полем "ssid". request (WifiDisconnectRequest): Объект с полем "ssid".
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с сообщением об успешном отключении. dict: Словарь с сообщением об успешном отключении.
@ -277,18 +312,26 @@ def disconnect_from_wifi(request: WifiDisconnectRequest):
Raises: Raises:
HTTPException: Если отключение не удалось (status_code=400). HTTPException: Если отключение не удалось (status_code=400).
""" """
wifi_service = WifiConnectionService() try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.disconnect(request.ssid) success, message = wifi_service.disconnect(request.ssid)
if not success: if not success:
raise HTTPException(status_code=400, detail=message) raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"message": message or f"Успешно отключено от {request.ssid}"} return {"message": translations.gettext(message or "Успешно отключено от {ssid}").format(ssid=request.ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка отключения от Wi-Fi: {error}").format(error=str(e)))
@router.get("/wifi/status") @router.get("/wifi/status")
def get_wifi_status(): def get_wifi_status(translations: Translations = Depends(get_translations)):
"""Проверяет статус текущего подключения к Wi-Fi. """Проверяет статус текущего подключения к Wi-Fi.
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно. Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.
Args:
translations: Объект переводов для текущего языка.
Returns: Returns:
dict: Словарь с ключами: dict: Словарь с ключами:
- "status" (str): Состояние ("connected" или другое). - "status" (str): Состояние ("connected" или другое).
@ -297,8 +340,13 @@ def get_wifi_status():
Raises: Raises:
HTTPException: Если проверка статуса не удалась (status_code=400). HTTPException: Если проверка статуса не удалась (status_code=400).
""" """
wifi_service = WifiConnectionService() try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.get_status() success, message = wifi_service.get_status()
if not success: if not success:
raise HTTPException(status_code=400, detail=message) raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"status": "connected", "ssid": message} return {"status": translations.gettext("connected"), "ssid": message}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка проверки статуса Wi-Fi: {error}").format(error=str(e)))

View file

@ -9,14 +9,29 @@ router = APIRouter()
class RuleNumbers(BaseModel): class RuleNumbers(BaseModel):
"""Модель запроса на удаление UFW правил.
Атрибуты:
- ``numbers`` (List[int]): Список номеров правил для удаления.
"""
numbers: List[int] = Field( numbers: List[int] = Field(
..., description="Список номеров правил для удаления") ..., description="Список номеров правил для удаления")
@router.get("/rules", response_model=UFWRulesResponse) @router.get("/rules", response_model=UFWRulesResponse)
async def get_ufw_rules(): async def get_ufw_rules():
""" """Возвращает все правила UFW.
Получает все правила UFW с помощью сервиса UFWService и возвращает их в формате JSON.
Осуществляет обращение к сервису `UFWService` для чтения текущего списка
правил брандмауэра и возвращает результат в сериализованном виде для клиента.
Returns:
UFWRulesResponse: Структура с коллекцией правил и сопутствующей
информацией о состоянии.
Raises:
HTTPException: В случае ошибок взаимодействия с UFW (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:
@ -27,8 +42,13 @@ async def get_ufw_rules():
@router.get("/status", response_model=UFWStatus) @router.get("/status", response_model=UFWStatus)
async def get_ufw_status(): async def get_ufw_status():
""" """Возвращает текущий статус UFW.
Получает текущий статус UFW.
Returns:
UFWStatus: Объект со сведениями о включенности UFW и деталями статуса.
Raises:
HTTPException: Если не удалось получить статус (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:
@ -39,8 +59,13 @@ async def get_ufw_status():
@router.post("/enable") @router.post("/enable")
async def enable_ufw(): async def enable_ufw():
""" """Включает UFW.
Включает UFW.
Returns:
dict: Словарь с ключами `status` и `message` при успешном выполнении.
Raises:
HTTPException: Если включение завершилось неуспешно (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:
@ -56,8 +81,13 @@ async def enable_ufw():
@router.post("/disable") @router.post("/disable")
async def disable_ufw(): async def disable_ufw():
""" """Выключает UFW.
Выключает UFW.
Returns:
dict: Словарь с ключами `status` и `message` при успешном выполнении.
Raises:
HTTPException: Если выключение завершилось неуспешно (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:
@ -73,8 +103,19 @@ async def disable_ufw():
@router.post("/rules/in") @router.post("/rules/in")
async def add_ufw_in_rules(rules: List[UFWRule]): async def add_ufw_in_rules(rules: List[UFWRule]):
""" """Добавляет входящие (in) правила UFW.
Добавляет входящие (in) правила UFW.
Args:
rules (List[UFWRule]): Список правил, определяющих направления,
порты/диапазоны/протоколы и действия (allow/deny/reject).
Returns:
dict: Итог выполнения с количеством успешно добавленных правил и деталями
по каждому правилу.
Raises:
HTTPException: Если UFW отключен (status_code=400) или при внутренних
ошибках (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:
@ -108,8 +149,17 @@ async def add_ufw_in_rules(rules: List[UFWRule]):
@router.post("/rules/out") @router.post("/rules/out")
async def add_ufw_out_rules(rules: List[UFWRule]): async def add_ufw_out_rules(rules: List[UFWRule]):
""" """Добавляет исходящие (out) правила UFW.
Добавляет исходящие (out) правила UFW.
Args:
rules (List[UFWRule]): Список правил исходящего трафика для добавления.
Returns:
dict: Объект статуса с сообщением об успешном добавлении.
Raises:
HTTPException: Если UFW отключен (status_code=400) или при внутренних
ошибках (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:
@ -137,8 +187,18 @@ async def add_ufw_out_rules(rules: List[UFWRule]):
@router.delete("/rules") @router.delete("/rules")
async def delete_ufw_rules(rule_numbers: RuleNumbers): async def delete_ufw_rules(rule_numbers: RuleNumbers):
""" """Удаляет правила UFW по их номерам.
Удаляет правила UFW по их номерам.
Args:
rule_numbers (RuleNumbers): Номера правил, подлежащих удалению (как в
выводе `ufw status numbered`).
Returns:
dict: Информация об общем количестве удаляемых правил, числе успешных и
возможной ошибке.
Raises:
HTTPException: При ошибках удаления (status_code=500).
""" """
ufw_service = UFWService() ufw_service = UFWService()
try: try:

View file

@ -2,8 +2,10 @@ import asyncio
import logging import logging
from typing import Set from typing import Set
from fastapi import APIRouter, WebSocket, WebSocketDisconnect from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
from starlette.websockets import WebSocketState from starlette.websockets import WebSocketState
from utils.i18n import get_translations_ws, _, _load_translations
from babel.support import Translations
from .network import update_container_position, get_network_data from .network import update_container_position, get_network_data
@ -70,15 +72,13 @@ class WebSocketManager:
except WebSocketDisconnect: except WebSocketDisconnect:
self.disconnect(connection) self.disconnect(connection)
except Exception as e: except Exception as e:
logger.error(f"Сообщение об ошибке трансляции: {e}") logger.error(f"Broadcast error: {e}")
self.disconnect(connection) self.disconnect(connection)
manager = WebSocketManager() manager = WebSocketManager()
@router.websocket("/api/v1") @router.websocket("/api/v1")
async def websocket_endpoint(websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket, translations: Translations = Depends(get_translations_ws)):
"""Обработчик WebSocket-соединения. """Обработчик WebSocket-соединения.
Управляет соединением клиента, принимает данные для обновления позиций Управляет соединением клиента, принимает данные для обновления позиций
@ -86,6 +86,7 @@ async def websocket_endpoint(websocket: WebSocket):
Args: Args:
websocket (WebSocket): Объект WebSocket для обработки соединения. websocket (WebSocket): Объект WebSocket для обработки соединения.
translations: Объект переводов для текущего языка.
Raises: Raises:
WebSocketDisconnect: Если клиент отключился. WebSocketDisconnect: Если клиент отключился.
@ -95,26 +96,26 @@ async def websocket_endpoint(websocket: WebSocket):
try: try:
while True: while True:
data = await websocket.receive_json() data = await websocket.receive_json()
logger.info(f"Полученные данные: {data}") logger.info(f"Received data: {data}")
if all(key in data for key in ["container_id", "container_x", "container_y"]): if all(key in data for key in ["container_id", "container_x", "container_y"]):
await update_container_position(data) await update_container_position(data, translations=translations)
network_data = await get_network_data() # Для внутреннего вызова используем переводы из WebSocket зависимостии
logger.info(f"Текущие сетевые данные: {network_data}") network_data = await get_network_data(translations=translations)
logger.info(f"Current network data: {network_data}")
await manager.broadcast(network_data) await manager.broadcast(network_data)
await asyncio.sleep(2) await asyncio.sleep(2)
except WebSocketDisconnect: except WebSocketDisconnect:
logger.info("Клиент отключен") logger.info("Client disconnected")
except Exception as e: except Exception as e:
logger.error(f"Ошибка в websocket_endpoint:{e}") logger.error(f"Error in websocket_endpoint: {e}")
finally: finally:
manager.disconnect(websocket) manager.disconnect(websocket)
if websocket.client_state != WebSocketState.DISCONNECTED: if websocket.client_state != WebSocketState.DISCONNECTED:
await websocket.close() await websocket.close()
@router.on_event("startup") @router.on_event("startup")
async def startup_event(): async def startup_event():
"""Запуск таска для трансляции сетевых данных при старте приложения. """Запуск таска для трансляции сетевых данных при старте приложения.
@ -126,27 +127,28 @@ async def startup_event():
""" """
asyncio.create_task(broadcast_data()) asyncio.create_task(broadcast_data())
async def broadcast_data(translations: Translations = None):
async def broadcast_data():
"""Таск для периодической трансляции сетевых данных. """Таск для периодической трансляции сетевых данных.
Выполняет сканирование сети и отправляет данные всем подключённым клиентам Выполняет сканирование сети и отправляет данные всем подключённым клиентам
с интервалом в 2 секунды. с интервалом в 2 секунды.
Args:
translations: Объект переводов для текущего языка.
Raises: Raises:
Exception: Если произошла ошибка при получении данных или трансляции. Exception: Если произошла ошибка при получении данных или трансляции.
""" """
translations = translations or _load_translations("ru")
while True: while True:
try: try:
network_data = await get_network_data() network_data = await get_network_data(translations=translations)
if "activation" not in network_data: if "activation" not in network_data:
logger.error( logger.error(translations.gettext("Ошибка: Ключ 'activation' отсутствует в полученных данных сети"))
"Ошибка: Ключ 'activation' отсутствует в полученных данных сети"
)
await asyncio.sleep(2) await asyncio.sleep(2)
continue continue
await manager.broadcast(network_data) await manager.broadcast(network_data)
except Exception as e: except Exception as e:
logger.error(f"Ошибка в broadcast_data:{e}") logger.error(f"Error in broadcast_data: {e}")
await asyncio.sleep(2) await asyncio.sleep(2)

3
SRV/babel.cfg Normal file
View file

@ -0,0 +1,3 @@
[python: **.py]
encoding = utf-8
extract_messages = _

0
SRV/config/__init__.py Normal file
View file

View file

@ -1,3 +1,20 @@
"""
Конфигурация CORS для FastAPI-приложения GONETS.
Модуль инкапсулирует добавление middleware `CORSMiddleware` со списком
разрешённых источников (origins), характерных для локальной разработки и
типовых портов фронтенда (например, Vite/React на 5173/5174, dev-серверы).
Использование:
cors_config = CORSMiddlewareConfig(app)
cors_config.add_cors_middleware()
Поведение:
- Разрешены все методы (`allow_methods=["*"]`) и заголовки (`allow_headers=["*"]`).
- Разрешены креденшлы (`allow_credentials=True`) для работы с куками (локаль и др.).
- При необходимости список `origins` можно расширить или параметризовать
на основе конфигурационного файла окружения.
"""
from fastapi import FastAPI from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware

View file

@ -1,4 +1,3 @@
# init_config.py
import configparser import configparser
import os import os
import secrets import secrets
@ -9,7 +8,18 @@ logger = logging.getLogger(__name__)
def init_config(): def init_config():
"""Создаёт конфигурационный файл с SECRET_KEY""" """
Инициализация конфигурации приложения (JWT SECRET_KEY).
Назначение:
- Создаёт файл `/etc/gonets/config.ini` с секцией `[JWT]` и случайным
`secret_key`, если он отсутствует. Устанавливает права `0o600`.
Замечания:
- Функция предназначена для однократного запуска (в инсталляторе/скрипте
подготовки окружения) и безопасно пропускает работу, если файл уже существует.
"""
"""Создаёт конфигурационный файл с SECRET_KEY."""
config_path = "/etc/gonets/config.ini" config_path = "/etc/gonets/config.ini"
config_dir = os.path.dirname(config_path) config_dir = os.path.dirname(config_path)
@ -34,6 +44,6 @@ def init_config():
) )
raise raise
# Нативный запуск для тестов
if __name__ == "__main__": if __name__ == "__main__":
init_config() init_config()

View file

@ -2,7 +2,13 @@ from typing import Dict, List
class PortConfig: class PortConfig:
"""Конфигурация портов для сетевого сканирования.""" """Конфигурация известных портов для сетевого анализа.
Назначение:
Предоставляет сопоставление распространённых портов с человекочитаемыми
названиями сервисов для отображения в UI и логах. Поддерживает расширение
пользовательским словарём.
"""
_DEFAULT_PORTS: Dict[int, str] = { _DEFAULT_PORTS: Dict[int, str] = {
20: "FTP (Data)", 20: "FTP (Data)",

View file

@ -3,8 +3,11 @@ from typing import Tuple
class CLIExecutor: class CLIExecutor:
""" """Утилита для безопасного выполнения CLI-команд.
Класс для выполнения CLI-команд
Назначение:
Инкапсулирует вызов `subprocess.run` и приводит результат к удобному
кортежу (код возврата, stdout) без генерации исключений при ошибках.
""" """
@staticmethod @staticmethod

View file

@ -0,0 +1,2 @@
[JWT]
secret_key = sphinx-docs-secret

View file

@ -0,0 +1 @@
{"sessions": {}}

View file

@ -0,0 +1 @@
{"users": [], "next_id": 1}

4
SRV/docs/_build/html/.buildinfo vendored Normal file
View file

@ -0,0 +1,4 @@
# Sphinx build info version 1
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 0de2fdeef2a39cc42113efadfd5a855d
tags: 645f666f9bcd5a90fca523b33c5a78b7

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more