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
.vendor
.python3-deb
SRV/storage/devices/devices.json
SRV/storage/subnets/*
SRV/storage/wifi/wifi.json
build/

View file

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

View file

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

View file

@ -115,7 +115,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
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(() => {
testSocket.close();
resolve(false);

View file

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

View file

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

View file

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

View file

@ -1,19 +1,11 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
on: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
quitApp: () => {
ipcRenderer.send('quit-app');
},
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
quitApp: () => ipcRenderer.send('quit-app'),
onThemeChange: (callback) => {
ipcRenderer.on('theme-update', (event, theme) => {
callback(theme);
});
}
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
writeFile: (path, data) => ipcRenderer.invoke('write-file', { path, data }),
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";
const axiosWithoutCredentials = axios.create({
baseURL: `https://localhost:8001`,
baseURL: `http://localhost:8001`,
})
export class NetworkService {
static createAxiosInstance(ipAddress: string) {
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 {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices');
return await axiosInstance.post('/subnet/devices',{
subnet: ipScanning,
});
} catch (error: any) {
return error.response;
}
@ -157,6 +159,8 @@ export class NetworkService {
return error.response;
}
}
// Settings
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
@ -173,4 +177,20 @@ export class NetworkService {
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 {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
@ -19,53 +20,55 @@ interface Props {
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const { ipAddress } = useIpStore();
const {ipAddress} = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleSave = async () => {
try {
setIsLoading(true);
setError('')
setError('');
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if(res.status === 200) {
onClose()
}else{
setError(res.data.detail)
if (res.status === 200) {
onClose();
} else {
setError(res.data.detail);
}
} catch (error) {
console.error('Error connection:', error);
setError('Ошибка подключения к сети')
}finally {
console.error(t('connectionErrorLog'), error);
setError(t('connectionError'));
} finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{ height: '230px' }}>
<ModalContainer style={{height: '230px'}}>
<ModalTitle>
<h3>Подключение к сети</h3>
<h3>{t('networkConnection')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<Input
type="password"
placeholderText='Введите пароль...'
placeholderText={t('enterPassword')}
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
{error}
</p>
{error && (
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
{error}
</p>
)}
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
@ -73,12 +76,12 @@ export const ConnectNetwork = ({address, onClose}: Props) => {
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title='Подключиться'
title={t('connect')}
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
{isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
</ModalContainer>
);
};

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer }
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={
message: string
@ -10,28 +11,37 @@ type Props={
export const ConnectionError = ({message}: Props)=>{
const {closeModal} = useModalContext()
const { setIpAddress } = useIpStore();
const {t} = useTranslation('common');
const close = ()=>{
setIpAddress('localhost')
closeModal()
}
return(
return (
<ModalContainer>
<ModalTitle>
<h3>Соединение прервано</h3>
<h3>{t('connectionLost')}</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Произошёл разрыв с сервером</h3>
<p>Пожалуйста, проверьте доступ к адресу и попробуйте снова.</p>
<h3>{t('serverDisconnected')}</h3>
<p>{t('checkAddressAndRetry')}</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Закрыть" onClick={close} size="small"/>
<MainButton title="Повторить" onClick={closeModal} size="small"/>
<MainButton
title={t('close')}
onClick={close}
size="small"
/>
<MainButton
title={t('retry')}
onClick={closeModal}
size="small"
/>
</ButtonContainer>
</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 RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={
address: string,
@ -16,6 +17,7 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleDisconnect = async () => {
try {
@ -34,21 +36,32 @@ export const DisconnectConfirm = ({address, onClose}: Props)=>{
return(
<ModalContainer>
<ModalTitle>
<h3>Отключение от Wi-Fi сети</h3>
<h3>{t('wifiDisconnect-title')}</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Подтверждение отключения</h3>
<h3>{t('wifiDisconnect-confirm')}</h3>
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Отменить" onClick={onClose} size="small"/>
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
<MainButton
title={t('cancel')}
onClick={onClose}
size="small"
/>
<MainButton
title={t('wifiDisconnect-disconnect')}
color={theme.errorColor}
hoverColor={theme.errorColor}
onClick={handleDisconnect}
size="small"
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
{isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
</ModalContainer>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,9 @@ import {buttonNames} from "../../const/buttons/names";
import SettingSelect from "./SettingSelect";
import {MainButton} from "../UI/buttons/mainButton";
import React from 'react';
import {useModalContext} from "../../store";
import {ModalMap} from "../modalwindows/windowsType";
import {useTranslation} from "../../utils/i18n/hooks";
const SettingBlock = styled.div`
display: flex;
@ -19,6 +22,7 @@ const SettingBlock = styled.div`
font-size: 16px;
outline: none;
background-color: ${props => props.theme.primary};
color: ${props => props.theme.textColor};
border: 1px solid ${props => props.theme.borderColor};
padding: 6px 8px;
border-radius: 4px;
@ -73,7 +77,7 @@ const InputWrapper = styled.div`
gap: 6px;
`
const ErrorText = styled.div`
color: red;
color: ${props => props.theme.errorColor};
font-size: 12px;
`;
@ -86,26 +90,65 @@ interface SettingItemProps {
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
const theme = useTheme();
const { t } = useTranslation('common');
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 inputValue = e.target.value;
let newError: string | null = null;
if (inputValue.trim() === '') {
newError = 'Поле не может быть пустым';
newError = t('settings-fieldRequired');
onChange(null);
}
else if (isNaN(Number(inputValue))) {
newError = 'Пожалуйста, введите число';
newError = t('settings-enterNumber');
}
else {
const numValue = Number(inputValue);
if (setting.min !== undefined && numValue < setting.min) {
newError = `Значение не может быть меньше ${setting.min}`;
newError = t('settings-valueTooSmall') + setting.min;
}
else if (setting.max !== undefined && numValue > setting.max) {
newError = `Значение не может быть больше ${setting.max}`;
newError = t('settings-valueTooLarge') + setting.max;
}
if (!newError) {
@ -172,9 +215,10 @@ const SettingItem = React.memo(({ setting, value, onChange, onValidationError }:
type="button"
size="small"
title={setting.defaultValue || ''}
onClick={setting.action}
onClick={handleButtonAction}
/>
)}
{setting.type === 'hidden' && null}
</SettingBlock>
);
});

View file

@ -1,13 +1,13 @@
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
import { MainButton } from "../UI/buttons/mainButton";
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 {SettingItem} from "./SettingItem";
import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names";
import {ModalMap} from "../modalwindows/windowsType";
import {workSpaceNames} from "../../const/workspace/names";
import {useTranslation} from "../../utils/i18n/hooks";
interface Props {
onClose: () => void;
@ -49,6 +49,16 @@ const SectionLine = styled.span`
const CloseBtn = styled.span`
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) => {
const settingsConfig = useSettingsStore(state => state.settingsConfig);
const settingsValues = useSettingsStore(state => state.settingsValues);
@ -59,10 +69,16 @@ export const Settings = ({ onClose }: Props) => {
const applyChanges = useSettingsStore(state => state.applyChanges);
const discardChanges = useSettingsStore(state => state.discardChanges);
const getSetting = useSettingsStore(state => state.getSetting);
const resetToDefaults = useSettingsStore(state => state.resetToDefaults);
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 [currentLanguage, setCurrentLanguage] = useState(language);
// Проверка действий
const handleValidationError = useCallback((fieldId: string, error: string | null) => {
setFieldErrors(prev => {
const newErrors = { ...prev };
@ -77,22 +93,33 @@ export const Settings = ({ onClose }: Props) => {
const settingsBySection = useMemo(() => {
return settingsConfig.reduce((acc, setting) => {
if (!acc[setting.sectionId]) {
acc[setting.sectionId] = [];
}
acc[setting.sectionId].push({
// Применяем переводы к настройкам
const translatedSetting = {
...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
});
};
if (!acc[translatedSetting.sectionId]) {
acc[translatedSetting.sectionId] = [];
}
acc[translatedSetting.sectionId].push(translatedSetting);
return acc;
}, {} as Record<string, Setting[]>);
}, [settingsConfig, settingsValues]);
}, [settingsConfig, settingsValues, t]);
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
useEffect(() => {
updateSettingsConfig();
initializeSettings();
}, [initializeSettings]);
}, [updateSettingsConfig, initializeSettings]);
useEffect(() => {
if (sections.length > 0 && !activeSection) {
@ -100,10 +127,15 @@ export const Settings = ({ onClose }: Props) => {
}
}, [sections, activeSection, setActiveSection]);
// Обновляем текущий язык при изменении
useEffect(() => {
setCurrentLanguage(language);
}, [language]);
const handleSave = useCallback(() => {
const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
if (hasErrors) {
alert('Исправьте ошибки перед сохранением');
alert(t('settings-fixErrors'));
return;
}
@ -114,13 +146,24 @@ export const Settings = ({ onClose }: Props) => {
});
if (emptyRequiredFields.length > 0) {
alert('Заполните все обязательные поля');
alert(t('settings-fillRequired'));
return;
}
// Проверяем, изменился ли язык
const newLanguage = pendingChanges['language'] ?? settingsValues['language'];
const languageChanged = newLanguage && newLanguage !== currentLanguage;
applyChanges();
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(() => {
discardChanges();
@ -128,10 +171,31 @@ export const Settings = ({ onClose }: Props) => {
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 (
<ModalContainer style={{ height: '60vh', width: '50vw'}}>
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle>
<h3>Настройки</h3>
<h3>{t('settings-title')}</h3>
<CloseBtn>
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
</CloseBtn>
@ -168,13 +232,32 @@ export const Settings = ({ onClose }: Props) => {
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
<SettingsButtons>
<MainButton
type="button"
size="small"
title={t('settings-resetSettings')}
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>
</ModalContainer>
);

View file

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

View file

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

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 {usePaginateSetting} from "../../../../../../hooks/useSettings";
import Pagination from "../../../../../UI/Pagination";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button`
position: relative;
@ -27,6 +28,7 @@ interface ListUsersProps {
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
const activeDevices = 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);
}
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы'}
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn>
{devices.length > 0 ? (
@ -70,7 +71,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
}}>
{/* Колонка активных устройств */}
<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) => (
<DeviceCard
key={index}
@ -81,7 +84,9 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
</div>
{/* Колонка неактивных устройств */}
<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) => (
<DeviceCard
key={index}
@ -91,8 +96,8 @@ const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps
))}
</div>
</div>
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Узлы не найдены</p>
) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null}
{totalPages > 1 && (
<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';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components';
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
import Pagination from "../../../../../UI/Pagination";
import {useSettingsStore} from "../../../../../../store/settings/settingsStore";
import {useNameSetting, usePaginateSetting} from "../../../../../../hooks/useSettings";
import {
useDisabledColumns,
useNameSetting,
useOrdersColumns,
usePaginateSetting
} from "../../../../../../hooks/useSettings";
interface TableScannerProps {
devices: DeviceUser[];
}
const TableContainer = styled.div`
width: 100%;
@ -65,87 +70,199 @@ const StatusBadge = styled.span<{ status:string }>`
border-radius:4px;
`
const TableScanner = ({devices}:TableScannerProps) => {
export const TableScanner = ({ devices }: TableScannerProps) => {
const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const ordersColumns = useOrdersColumns();
const disabledColumns = useDisabledColumns();
const paginateSetting = usePaginateSetting();
const {t} = useTranslation('common');
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
console.log(ordersColumns);
useEffect(() => {
setDevicesPerPage(paginateSetting);
}, [paginateSetting]);
const openModalNote = (e: React.MouseEvent, device: DeviceUser) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
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 indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
const currentDevices = devices.slice(indexOfFirstDevice, indexOfLastDevice);
const totalPages = Math.ceil(devices.length / devicesPerPage);
const handlePrevPage = () => {
if(currentPage > 1) setCurrentPage(currentPage - 1);
}
if (currentPage > 1) setCurrentPage(currentPage - 1);
};
const handleNextPage = () => {
if(currentPage < totalPages) setCurrentPage(currentPage + 1);
}
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
};
const onPageChange = (page: number) => {
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 (
<TableContainer>
<StyledTable>
<thead>
<tr>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Имя</TableHeaderCell>
<TableHeaderCell>Производитель</TableHeaderCell>
<TableHeaderCell>IP адрес</TableHeaderCell>
<TableHeaderCell>Открытые порты</TableHeaderCell>
<TableHeaderCell>Последняя активность</TableHeaderCell>
<TableHeaderCell>Заметка</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</tr>
</thead>
<tbody>
{currentDevices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<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>
))}
</tbody>
</StyledTable>
{totalPages > 1 && (
<Pagination
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
)}
</TableContainer>
);
};
<TableContainer>
<StyledTable>
<thead>
<tr>
{visibleColumns.map(column => {
switch (column) {
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
case 'name': return <TableHeaderCell key="name">{t('settings-name')}</TableHeaderCell>;
case 'vendor': return <TableHeaderCell key="vendor">{t('settings-vendor')}</TableHeaderCell>;
case 'ip': return <TableHeaderCell key="ip">{t('settings-ipAddress')}</TableHeaderCell>;
case 'ports': return <TableHeaderCell key="ports">{t('settings-openPorts')}</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>
</thead>
<tbody>
{currentDevices.map((item, index) => (
<TableRow key={index}>
{visibleColumns.map(column => renderTableCell(column, item))}
</TableRow>
))}
</tbody>
</StyledTable>
export default TableScanner;
{totalPages > 1 && (
<Pagination
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
)}
</TableContainer>
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { IconComponent } from "../../../../const/buttons/icons"
import { buttonNames } from "../../../../const/buttons/names"
import {useTranslation} from "../../../../utils/i18n/hooks";
const Container = styled.div`
display: flex;
@ -102,11 +103,12 @@ interface CurrentDevice {
}
export const DevicesBar = () => {
const { openModal } = useModalContext();
const { setIpAddress, ipAddress } = useIpStore();
const { devices, addDevice, deleteDevice } = useDevicesStore()
const { isOpen, collapseMenu } = useMenu(menusNames[1]);
const { setWorkModule } = useWorkSpaceStore()
const {openModal} = useModalContext();
const {setIpAddress, ipAddress} = useIpStore();
const {devices, addDevice, deleteDevice} = useDevicesStore()
const {isOpen, collapseMenu} = useMenu(menusNames[1]);
const {setWorkModule} = useWorkSpaceStore()
const {t} = useTranslation('common');
const [currentDevice, setCurrentDevice] = useState<CurrentDevice>({
address: 'localhost',
@ -115,7 +117,7 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
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(() => {
testSocket.close();
resolve(false);
@ -164,7 +166,7 @@ export const DevicesBar = () => {
deleteDevice(number)
if(adress == ipAddress){
if (adress == ipAddress) {
setIpAddress('localhost')
}
};
@ -173,7 +175,7 @@ export const DevicesBar = () => {
const isAvailable = await checkWebSocketAvailability(ip);
if (!isAvailable) {
openModal(ModalMap['3'], {
message: `Не удалось подключиться к серверу по адресу ${ip}`,
message: t('nodes-connectionError') + ip,
});
return;
}
@ -191,44 +193,46 @@ export const DevicesBar = () => {
return (
<Container>
<Title>
<p className="unselectable">Доступные узлы</p>
<p className="unselectable">{t('nodes-availableNodes')}</p>
</Title>
<DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Корневой адрес</p>
<p>{t('nodes-rootAddress')}</p>
</DeviceControl>
{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>
<DevicesContainer>
<DevicesHeader className="unselectable" onClick={collapseMenu}>
<p>Дополнительные узлы</p>
<IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]} />
<p>{t('nodes-additionalNodes')}</p>
<IconComponent name={isOpen ? buttonNames[5] : buttonNames[6]}/>
</DevicesHeader>
<DevicesContent $isactive={isOpen.toString()} >
<DevicesContent $isactive={isOpen.toString()}>
{devices.length > 0 &&
devices.map((device, index) => (
<DeviceBlock key={index}>
<DeviceControl className="unselectable" onClick={() => switchWs(device.address)}>
<p>Узел {device.number}</p>
<p>{t('nodes-node')} {device.number}</p>
<IconComponent
width="20"
height="20"
name={buttonNames[2]}
setColor="white"
onClick={(e: any) => {
e.stopPropagation(); // Предотвращаем всплытие события
e.stopPropagation();
handleDelete(device.number, device.address);
}}
/>
</DeviceControl>
<p className="paddingP">Адрес: {device.address}</p>
<p className="paddingP">{t('nodes-address')}: {device.address}</p>
</DeviceBlock>
))
}
@ -237,7 +241,6 @@ export const DevicesBar = () => {
+
</PlusBlock>
</DevicesContent>
</DevicesContainer>
</Container>
)

View file

@ -2,4 +2,8 @@ interface Window {
electron: {
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 {defaultLanguage, Language} from "../utils/i18n/config";
export const useNameSetting = () => {
return useSettingsStore(state => state.getSettingValue('netnames-nodes')) as string;
@ -8,6 +9,11 @@ export const usePaginateSetting = () => {
return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20));
};
export const useThemeSettings = () => {
return useSettingsStore(state => state.getSettingValue('theme'));
};
//Интервальное сканирование под узлов
export const useIntervalBoolean = () => {
return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean;
};
@ -15,6 +21,17 @@ export const useIntervalTime = () => {
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>()(
persist(
(set)=>({
ipAddress: '192:168.1.147',
ipAddress: '192:168.1.0',
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
}),
{

View file

@ -1,25 +1,30 @@
export const appearanceSettings = [
{
id: 'theme',
sectionId: 'Внешний вид',
label: 'Цветовая тема',
type: 'select',
defaultValue: 'light',
options: [
{ value: 'light', label: 'Светлая' },
{ value: 'dark', label: 'Тёмная' },
{ value: 'system', label: 'Как в системе' }
],
},
{
id: 'language',
sectionId: 'Внешний вид',
label: 'Язык интерфейса',
type: 'select',
defaultValue: 'ru',
options: [
{ value: 'ru', label: 'Русский' },
{ value: 'en', label: 'Английский' },
],
},
] as const;
const createAppearanceSettings = () => {
return [
{
id: 'theme',
sectionId: 'settings-sections-appearance',
label: 'settings-colorTheme',
type: 'select',
defaultValue: 'light',
options: [
{ value: 'light', label: 'settings-light' },
{ value: 'dark', label: 'settings-dark' },
{ value: 'system', label: 'settings-system' }
],
},
{
id: 'language',
sectionId: 'settings-sections-appearance',
label: 'settings-interfaceLanguage',
type: 'select',
defaultValue: 'ru',
options: [
{ value: 'ru', label: 'settings-russian' },
{ value: 'en', label: 'settings-english' },
],
},
] 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',
type: 'button',
label: 'Очистить данные сессии',
sectionId: 'Точки доступа',
defaultValue: 'Очистить',
label: 'settings-clearSessionData',
sectionId: 'settings-sections-accessPoints',
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',
type: 'button',
label: 'Экспорт данных точек доступа',
sectionId: 'Точки доступа',
defaultValue: 'Экспортировать',
label: 'settings-exportNetworkData',
sectionId: 'settings-sections-accessPoints',
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,59 +1,119 @@
export const nodesSettings = [
{
id: 'rescan-nodes',
sectionId: 'Узлы подсети',
label: 'Интервальное сканирование',
type: 'boolean',
defaultValue: false,
},
{
id: 'interval-nodes',
sectionId: 'Узлы подсети',
label: 'Интервал сканирования узлов (мин)',
type: 'number',
defaultValue: 60,
min: 1,
max: 2880
},
{
id: 'netnames-nodes',
sectionId: 'Узлы подсети',
label: 'Отображаемые имена при отсутствии',
type: 'select',
defaultValue: 'ip',
options: [
{ value: 'ip', label: 'IP адрес' },
{ value: 'unknown', label: 'Неизвестно' },
],
},
{
id: 'paginate-nodes',
type: 'number',
label: 'Количество строк в таблице',
sectionId: 'Узлы подсети',
defaultValue: 20,
min: 1,
},
{
id: 'customize-nodes',
type: 'button',
label: 'Отображение узлов',
sectionId: 'Узлы подсети',
defaultValue: 'Настроить',
},
{
id: 'clean-nodes',
type: 'button',
label: 'Очистить данные сессии',
sectionId: 'Узлы подсети',
defaultValue: 'Очистить',
},
{
id: 'export-nodes',
type: 'button',
label: 'Экспорт данных узлов',
sectionId: 'Узлы подсети',
defaultValue: 'Экспортировать',
},
] as const;
import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNodesSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{
id: 'rescan-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-intervalScanning',
type: 'boolean',
defaultValue: false,
},
{
id: 'interval-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-scanInterval',
type: 'number',
defaultValue: 60,
min: 1,
max: 2880
},
{
id: 'netnames-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-displayNames',
type: 'select',
defaultValue: 'ip',
options: [
{ value: 'ip', label: 'settings-ipAddress' },
{ value: 'unknown', label: 'settings-unknown' },
],
},
{
id: 'paginate-nodes',
type: 'number',
label: 'settings-rowsPerPage',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 20,
min: 1,
},
{
id: 'customize-nodes',
type: 'button',
label: 'settings-displaySettings',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 'settings-configure',
},
{
id: 'clean-nodes',
type: 'button',
label: 'settings-clearSessionData',
sectionId: 'settings-sections-subnetNodes',
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",
type: "button",
label: "settings-exportData",
sectionId: "settings-sections-subnetNodes",
defaultValue: "settings-export",
action: async () => {
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 { persist } from 'zustand/middleware';
import {nodesSettings} from "./config/nodesSettings";
import { networkSettings } from './config/networkSettings';
import {appearanceSettings} from "./config/appearanceSettings";
import {createNodesSettings} from "./config/nodesSettings";
import { createNetworkSettings } from './config/networkSettings';
import {createAppearanceSettings} from "./config/appearanceSettings";
type SettingValue = any;
type SettingsDictionary = Record<string, SettingValue>;
interface SettingOption {
readonly value: any;
readonly label: string;
}
export type Setting = {
readonly id: string;
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button';
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button' | 'hidden';
readonly label: string;
readonly defaultValue: any;
value?: string;
@ -38,20 +33,16 @@ type SettingsState = {
resetSettings: () => void;
applyChanges: () => void;
discardChanges: () => void;
resetToDefaults: () => void;
getSettingValue: <T = any>(id: string) => T;
getNumberSetting: (id: string, fallback?: number) => number;
updateSettingsConfig: () => void;
};
const defaultSettings: Setting[] = [
...nodesSettings,
...networkSettings,
...appearanceSettings,
];
export const useSettingsStore = create<SettingsState>()(
persist(
(set, get) => ({
settingsConfig: defaultSettings,
settingsConfig: [],
settingsValues: {},
activeSection: null,
pendingChanges: {},
@ -63,6 +54,16 @@ export const useSettingsStore = create<SettingsState>()(
pendingChanges: { ...state.pendingChanges, [id]: value }
})),
resetToDefaults: () => set({
settingsValues: Object.fromEntries(
get().settingsConfig.map(setting => [
setting.id,
setting.defaultValue
])
),
pendingChanges: {}
}),
applyChanges: () =>
set((state) => ({
settingsValues: { ...state.settingsValues, ...state.pendingChanges },
@ -84,6 +85,15 @@ export const useSettingsStore = create<SettingsState>()(
set({ settingsValues: newValues });
},
updateSettingsConfig: () => {
const defaultSettings: Setting[] = [
...createNodesSettings(),
...createNetworkSettings(),
...createAppearanceSettings(),
];
set({ settingsConfig: defaultSettings });
},
getSetting: (id) => {
const state = get();
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 { GlobalStyle } from '../const/theme/globalStyles';
import { themeNames, ThemName } from '../const/theme/names';
import {useThemeSettings} from "../hooks/useSettings";
type ThemeContextType = {
theme: ThemName;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
@ -23,32 +23,13 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const getSystemTheme = () =>
window.matchMedia('(prefers-color-scheme: dark)').matches ? themeNames[2] : themeNames[1];
const getInitialTheme = () => {
let savedTheme = localStorage.getItem('theme') as ThemName;
if (!savedTheme) {
savedTheme = themeNames[3]; // Устанавливаем значение по умолчанию
localStorage.setItem('theme', savedTheme); // Сохраняем без JSON.stringify
}
return savedTheme;
};
const getInitialTheme = useThemeSettings();
const [theme, setTheme] = useState<ThemName>(getInitialTheme);
const toggleTheme = () => {
setTheme((prev) => {
let newTheme: ThemName;
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(() => {
setTheme(getInitialTheme)
}, [getInitialTheme]);
useEffect(() => {
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;
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark, toggleTheme }}>
<ThemeContext.Provider value={{ theme }}>
<StyledTheme theme={{ ...currentTheme === themeNames[1] ? light : dark }}>
<GlobalStyle />
{children}
</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:
"@types/node" "*"
"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^20.9.0":
version "20.17.17"
resolved "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz"
integrity sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==
"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.7.7":
version "22.19.1"
resolved "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz"
integrity sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==
dependencies:
undici-types "~6.19.2"
undici-types "~6.21.0"
"@types/prop-types@*":
version "15.7.13"
@ -743,12 +743,12 @@ author-regex@^1.0.0:
integrity sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==
axios@^1.7.9:
version "1.8.4"
resolved "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz"
integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==
version "1.13.2"
resolved "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz"
integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
form-data "^4.0.4"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0:
@ -772,9 +772,9 @@ boolean@^3.0.1:
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
version "1.1.12"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
@ -817,6 +817,14 @@ cacheable-request@^7.0.2:
normalize-url "^6.0.1"
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:
version "1.0.1"
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"
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:
version "17.1.2"
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"
integrity sha512-pZ5EcTWRq/055MvSBgoFEyKf2i4apwfoqJbK/ak2jnFq8oHjZ+vzc3AhRcz37Xn+ZJfL58R666FLJx0YOK9yTw==
electron@^34.0.2:
version "34.0.2"
resolved "https://registry.npmjs.org/electron/-/electron-34.0.2.tgz"
integrity sha512-u3F+DSUlg9NaGS+9qnYmSRN8VjAnc3LJDDk1ye1uISJnh4gjG76y3681qLowsPMx4obvCP2eBINnmbLo0yT5WA==
electron@^39.2.3:
version "39.2.3"
resolved "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz"
integrity sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
"@types/node" "^22.7.7"
extract-zip "^2.0.1"
end-of-stream@^1.1.0:
@ -1090,7 +1107,7 @@ error-ex@^1.2.0:
dependencies:
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"
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
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"
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:
version "4.1.1"
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"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
form-data@^4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz"
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
form-data@^4.0.4:
version "4.0.5"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz"
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
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"
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:
version "1.0.0"
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"
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:
version "5.2.0"
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"
gopd "^1.0.1"
gopd@^1.0.1:
gopd@^1.0.1, gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
@ -1380,6 +1440,18 @@ has-property-descriptors@^1.0.0:
dependencies:
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:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
@ -1559,6 +1631,11 @@ matcher@^3.0.0:
dependencies:
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:
version "1.52.0"
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"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
universalify@^0.1.0:
version "0.1.2"
@ -2162,9 +2239,9 @@ validate-npm-package-license@^3.0.1:
spdx-expression-parse "^3.0.0"
"vite@^4.2.0 || ^5.0.0 || ^6.0.0", vite@^6.0.0, vite@^6.3.4:
version "6.3.4"
resolved "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz"
integrity sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==
version "6.4.1"
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
dependencies:
esbuild "^0.25.0"
fdir "^6.4.4"

3
SRV/.gitignore vendored
View file

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

View file

@ -1,13 +1,14 @@
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 utils.i18n import get_translations
from babel.support import Translations
router = APIRouter()
ip_service = IPService()
@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-адреса перед его применением.
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
@ -16,6 +17,7 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса.
translations: Объект переводов для текущего языка.
Returns:
Dict[str, Any]: Словарь с ключами:
@ -26,16 +28,16 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
HTTPException: Если произошла внутренняя ошибка (status_code=500).
"""
try:
result = await ip_service.check_ip_config(data)
result = await ip_service.check_ip_config(data, translations)
return result
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
@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-настройки к указанному сетевому профилю.
Обновляет настройки соединения через NetworkManager, отключает и
@ -44,6 +46,7 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...)):
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
@ -55,19 +58,23 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...)):
произошла внутренняя ошибка (status_code=500).
"""
try:
await ip_service.apply_ip_config(data)
await ip_service.apply_ip_config(data, translations)
return {
"success": True,
"message": f"Настройки сетевого профиля для {data['connection_id']} применены",
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
}
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:
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")
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-настройки для отката сетевого профиля.
Используется для сохранения и отката настроек сетевого профиля к предыдущему
@ -76,6 +83,7 @@ async def save_ip_config(data: Dict[str, Any] = Body(...)):
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
@ -87,12 +95,17 @@ async def save_ip_config(data: Dict[str, Any] = Body(...)):
произошла внутренняя ошибка (status_code=500).
"""
try:
await ip_service.apply_ip_config(data)
await ip_service.apply_ip_config(data, translations)
return {
"success": True,
"message": f"Настройки сетевого профиля для {data['connection_id']} применены",
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
}
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:
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
from typing import Dict, Tuple, List, Union
from services.network_service import NetworkService
from models.network import (
NetworkInterfacesContainer,
@ -10,6 +9,8 @@ from models.network import (
NetworkInterface,
WiFiDevice,
)
from utils.i18n import get_translations, _, _load_translations
from babel.support import Translations
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@ -17,7 +18,6 @@ logger = logging.getLogger(__name__)
router = APIRouter()
network_service = NetworkService()
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:
container_name (str): Имя контейнера ("activation" или "deactivation").
translations: Объект переводов для текущего языка.
Returns:
Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen).
@ -58,7 +59,7 @@ class ContainerPositionManager:
"""
container = self.positions.get(container_name)
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"]
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"
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
container_manager = ContainerPositionManager()
async def get_network_data() -> Dict[str, Dict]:
async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]:
"""Возвращает текущие сетевые данные в виде словаря.
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные,
и возвращает их в формате Activation и Deactivation с учётом позиций и состояний
контейнеров.
Args:
translations: Объект переводов для текущего языка.
Returns:
Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими
данные о соответствующих контейнерах.
@ -94,14 +96,14 @@ async def get_network_data() -> Dict[str, Dict]:
Raises:
HTTPException: Если произошла ошибка при получении данных (status_code=500).
"""
logger.debug("Вызов get_network_data()")
logger.debug("Calling get_network_data()")
try:
network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation"))
deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation"))
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", translations)
activation = Activation(
id=1,
@ -123,9 +125,8 @@ async def get_network_data() -> Dict[str, Dict]:
"deactivation": deactivation.model_dump(),
}
except Exception as e:
logger.error(f"Ошибка в get_network_data: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
logger.error(f"Error in get_network_data: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить сетевые данные"))
def _split_interfaces(
interfaces: List[Union[NetworkInterface, WiFiDevice]],
@ -148,9 +149,8 @@ def _split_interfaces(
deactivation_interfaces.append(interface)
return activation_interfaces, deactivation_interfaces
@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_y" (int): Новая координата Y.
- "isOpen" (bool): Новое состояние контейнера (открыт/закрыт).
translations: Объект переводов для текущего языка.
Returns:
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):
raise ValueError(
"Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen"
translations.gettext("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
)
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_y, int)
or not isinstance(is_open, bool)):
raise ValueError("Неверный тип данных в запросе")
raise ValueError(translations.gettext("Неверный тип данных в запросе"))
container_manager.update_position(container_id, container_x, container_y, is_open)
return {
"success": True,
"message": "Позиция и состояние контейнера обновлены"
"message": translations.gettext("Позиция и состояние контейнера обновлены")
}
except Exception as e:
logger.error(f"Ошибка обновления позиции контейнера: {e}")
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
logger.error(f"Error updating container position: {e}")
raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e)))
@router.get("/get_hostname")
async def get_hostname():
async def get_hostname(translations: Translations = Depends(get_translations)):
"""Возвращает имя хоста системы.
Выполняет запрос к сервису для получения имени хоста.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключом "hostname" и значением имени хоста.
@ -218,5 +221,5 @@ async def get_hostname():
hostname_info = await network_service.get_hostname()
return hostname_info
except Exception as e:
logger.error(f"Ошибка в get_hostname: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")
logger.error(f"Error in get_hostname: {e}")
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 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()
settings_service = SettingsService()
@router.post("/clear_subnet")
def clear_subnet():
def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети, сохранённые в devices.json.
Удаляет все записи об узлах подсети, оставляя файл пустым.
Args:
subnet (Optional[str]): Подсеть в формате CIDR (например, "10.1.1.0/24").
Если None, очищает все подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
@ -17,18 +33,27 @@ def clear_subnet():
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_subnet()
success = settings_service.clear_subnet(subnet)
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:
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")
def clear_wifi():
def clear_wifi(translations: Translations = Depends(get_translations)):
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
Удаляет все записи о точках доступа, оставляя файл пустым.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
@ -38,16 +63,22 @@ def clear_wifi():
try:
success = settings_service.clear_wifi()
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:
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")
def clear_all_data():
def clear_all_data(translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
Удаляет все записи из devices.json и wifi.json.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
@ -57,6 +88,122 @@ def clear_all_data():
try:
success = settings_service.clear_all_data()
if success:
return {"message": "Все сетевые данные успешно очищены"}
return {"message": translations.gettext("Все сетевые данные успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить все сетевые данные"))
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 typing import List, Dict
from models.subnet import SubnetScanResult
from fastapi import APIRouter, HTTPException, Body, Depends
from models.subnet import SubnetScanResult, SubnetScanRequest
from models.wifi import WifiScanResult
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.connect_service import WifiConnectionService
from utils.i18n import get_translations, _
from babel.support import Translations
import logging
router = APIRouter()
@router.get("/devices", response_model=SubnetScanResult)
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
logging.basicConfig(level=logging.INFO)
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:
subnet (str, optional): Адрес подсети в формате CIDR (например, "192.168.1.0/24").
По умолчанию "192.168.1.0/24".
ports (List[int], optional): Список портов для сканирования. По умолчанию None.
request (SubnetScanRequest): Объект с полем subnet.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
SubnetScanResult: Объект с результатами сканирования (устройства и их данные).
@ -28,20 +32,26 @@ def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
HTTPException: Если произошла ошибка сканирования (status_code=500).
"""
try:
subnet_service = SubnetService()
result = subnet_service.scan_subnet(subnet, ports)
# Если subnet пустая строка, определяем автоматически
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
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)
def scan_wifi(timeout: int = 30):
def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них.
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
Args:
timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30.
translations: Объект переводов для текущего языка.
Returns:
WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей).
@ -54,10 +64,10 @@ def scan_wifi(timeout: int = 30):
result = wifi_service.scan_wifi(timeout=timeout)
return result
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")
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-адресу.
@ -65,6 +75,8 @@ def add_note(mac: str, request: NoteRequest):
Args:
mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном добавлении заметки.
@ -74,21 +86,22 @@ def add_note(mac: str, request: NoteRequest):
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
subnet_service = SubnetService()
success, error_message = subnet_service.add_note(mac, request.note)
success, result = subnet_service.add_note(mac, request.note, translations)
if not success:
if "не найдено" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "уже существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка '{request.note}' добавлена для устройства {mac}"}
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} уже существует").format(mac=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:
raise 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")
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-адресу.
Изменяет существующую заметку, привязанную к устройству.
@ -96,6 +109,8 @@ def update_note(mac: str, request: NoteRequest):
Args:
mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для нового текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном обновлении заметки.
@ -105,27 +120,30 @@ def update_note(mac: str, request: NoteRequest):
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
subnet_service = SubnetService()
success, error_message = subnet_service.update_note(mac, request.note)
success, result = subnet_service.update_note(mac, request.note, translations)
if not success:
if "не найдено" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка '{request.note}' обновлена для устройства {mac}"}
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=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:
raise 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")
def delete_note(mac: str):
def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Удаляет заметку для устройства по MAC-адресу.
Удаляет привязанную заметку для указанного устройства.
Args:
mac (str): MAC-адрес устройства.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном удалении заметки.
@ -135,21 +153,22 @@ def delete_note(mac: str):
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
subnet_service = SubnetService()
success, error_message = subnet_service.delete_note(mac)
success, result = subnet_service.delete_note(mac, translations)
if not success:
if "не найдено" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка удалена для устройства {mac}"}
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=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:
raise 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")
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.
@ -157,6 +176,7 @@ def add_wifi_note(ssid: str, request: NoteRequest):
Args:
ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для текста заметки.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном добавлении заметки.
@ -167,20 +187,22 @@ def add_wifi_note(ssid: str, request: NoteRequest):
"""
try:
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 "не найдена" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "уже существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка '{request.note}' добавлена для сети с SSID {ssid}"}
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} уже существует").format(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:
raise 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")
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 сети.
@ -188,6 +210,7 @@ def update_wifi_note(ssid: str, request: NoteRequest):
Args:
ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для нового текста заметки.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном обновлении заметки.
@ -198,26 +221,29 @@ def update_wifi_note(ssid: str, request: NoteRequest):
"""
try:
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 "не найдена" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка '{request.note}' обновлена для сети с SSID {ssid}"}
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(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:
raise 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")
def delete_wifi_note(ssid: str):
def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)):
"""Удаляет заметку для сети Wi-Fi по SSID.
Удаляет привязанную заметку для указанной Wi-Fi сети.
Args:
ssid (str): Идентификатор Wi-Fi сети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном удалении заметки.
@ -228,20 +254,22 @@ def delete_wifi_note(ssid: str):
"""
try:
wifi_service = WifiService()
success, error_message = wifi_service.delete_note(ssid)
success, result = wifi_service.delete_note(ssid)
if not success:
if "не найдена" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка удалена для сети с SSID {ssid}"}
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(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:
raise 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")
def connect_to_wifi(request: WifiConnectRequest):
def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)):
"""Подключается к Wi-Fi сети по SSID и паролю.
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
@ -249,6 +277,7 @@ def connect_to_wifi(request: WifiConnectRequest):
Args:
request (WifiConnectRequest): Объект с полями "ssid" и "password".
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном подключении.
@ -256,20 +285,26 @@ def connect_to_wifi(request: WifiConnectRequest):
Raises:
HTTPException: Если подключение не удалось (status_code=400).
"""
wifi_service = WifiConnectionService()
success, message = wifi_service.connect(request.ssid, request.password)
if not success:
raise HTTPException(status_code=400, detail=message)
return {"message": f"Успешно подключено к {request.ssid}"}
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.connect(request.ssid, request.password)
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
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")
def disconnect_from_wifi(request: WifiDisconnectRequest):
def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)):
"""Отключается от Wi-Fi сети по указанному SSID.
Выполняет отключение от указанной Wi-Fi сети.
Args:
request (WifiDisconnectRequest): Объект с полем "ssid".
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном отключении.
@ -277,18 +312,26 @@ def disconnect_from_wifi(request: WifiDisconnectRequest):
Raises:
HTTPException: Если отключение не удалось (status_code=400).
"""
wifi_service = WifiConnectionService()
success, message = wifi_service.disconnect(request.ssid)
if not success:
raise HTTPException(status_code=400, detail=message)
return {"message": message or f"Успешно отключено от {request.ssid}"}
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.disconnect(request.ssid)
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
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")
def get_wifi_status():
def get_wifi_status(translations: Translations = Depends(get_translations)):
"""Проверяет статус текущего подключения к Wi-Fi.
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "status" (str): Состояние ("connected" или другое).
@ -297,8 +340,13 @@ def get_wifi_status():
Raises:
HTTPException: Если проверка статуса не удалась (status_code=400).
"""
wifi_service = WifiConnectionService()
success, message = wifi_service.get_status()
if not success:
raise HTTPException(status_code=400, detail=message)
return {"status": "connected", "ssid": message}
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.get_status()
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(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):
"""Модель запроса на удаление UFW правил.
Атрибуты:
- ``numbers`` (List[int]): Список номеров правил для удаления.
"""
numbers: List[int] = Field(
..., description="Список номеров правил для удаления")
@router.get("/rules", response_model=UFWRulesResponse)
async def get_ufw_rules():
"""
Получает все правила UFW с помощью сервиса UFWService и возвращает их в формате JSON.
"""Возвращает все правила UFW.
Осуществляет обращение к сервису `UFWService` для чтения текущего списка
правил брандмауэра и возвращает результат в сериализованном виде для клиента.
Returns:
UFWRulesResponse: Структура с коллекцией правил и сопутствующей
информацией о состоянии.
Raises:
HTTPException: В случае ошибок взаимодействия с UFW (status_code=500).
"""
ufw_service = UFWService()
try:
@ -27,8 +42,13 @@ async def get_ufw_rules():
@router.get("/status", response_model=UFWStatus)
async def get_ufw_status():
"""
Получает текущий статус UFW.
"""Возвращает текущий статус UFW.
Returns:
UFWStatus: Объект со сведениями о включенности UFW и деталями статуса.
Raises:
HTTPException: Если не удалось получить статус (status_code=500).
"""
ufw_service = UFWService()
try:
@ -39,8 +59,13 @@ async def get_ufw_status():
@router.post("/enable")
async def enable_ufw():
"""
Включает UFW.
"""Включает UFW.
Returns:
dict: Словарь с ключами `status` и `message` при успешном выполнении.
Raises:
HTTPException: Если включение завершилось неуспешно (status_code=500).
"""
ufw_service = UFWService()
try:
@ -56,8 +81,13 @@ async def enable_ufw():
@router.post("/disable")
async def disable_ufw():
"""
Выключает UFW.
"""Выключает UFW.
Returns:
dict: Словарь с ключами `status` и `message` при успешном выполнении.
Raises:
HTTPException: Если выключение завершилось неуспешно (status_code=500).
"""
ufw_service = UFWService()
try:
@ -73,8 +103,19 @@ async def disable_ufw():
@router.post("/rules/in")
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()
try:
@ -108,8 +149,17 @@ async def add_ufw_in_rules(rules: List[UFWRule]):
@router.post("/rules/out")
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()
try:
@ -137,8 +187,18 @@ async def add_ufw_out_rules(rules: List[UFWRule]):
@router.delete("/rules")
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()
try:

View file

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

View file

@ -1,4 +1,3 @@
# init_config.py
import configparser
import os
import secrets
@ -9,7 +8,18 @@ logger = logging.getLogger(__name__)
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_dir = os.path.dirname(config_path)
@ -34,6 +44,6 @@ def init_config():
)
raise
# Нативный запуск для тестов
if __name__ == "__main__":
init_config()

View file

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

View file

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