Полная интернационализация

This commit is contained in:
test 2025-08-11 13:56:43 +05:00
parent 3315123029
commit 4286f28d1e
26 changed files with 1241 additions and 465 deletions

View file

@ -1,22 +1,186 @@
{ {
"userScanner-title": "Subnet nodes scanning", "userScanner-title": "Subnet Node Scanning",
"userScanner-visible": "Display mode", "userScanner-visible": "Display Method",
"userScanner-loading": "Scanning nodes", "userScanner-loading": "Scanning nodes",
"userScanner-refresh": "Refresh list", "userScanner-refresh": "Refresh List",
"userScanner-scan": "Scan nodes", "userScanner-scan": "Scan Nodes",
"userScanner-notFound": "No nodes found", "userScanner-notFound": "Nodes not found",
"tableScanner-online": "Online", "tableScanner-online": "Online",
"tableScanner-name": "Name", "tableScanner-name": "Name",
"tableScanner-vendor": "Manufacturer", "tableScanner-vendor": "Vendor",
"tableScanner-ip": "IP address", "tableScanner-ip": "IP Address",
"tableScanner-ports": "Open ports", "tableScanner-ports": "Open Ports",
"tableScanner-activity": "Last activity", "tableScanner-activity": "Last Activity",
"tableScanner-note": "Note", "tableScanner-note": "Note",
"tableScanner-action": "Action", "tableScanner-action": "Action",
"tableScanner-unknown": "Unknown", "tableScanner-unknown": "Unknown",
"tableScanner-noData": "No data to display", "tableScanner-noData": "No data to display",
"listUsers-activeNodes": "Active nodes", "listUsers-activeNodes": "Active Nodes",
"listUsers-inactiveNodes": "Inactive nodes", "listUsers-inactiveNodes": "Inactive Nodes",
"ipInput-placeholder": "Enter IP", "ipInput-placeholder": "Enter IP",
"ipInput-error": "Please enter a valid IP address" "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

@ -18,5 +18,175 @@
"listUsers-activeNodes": "Активные узлы", "listUsers-activeNodes": "Активные узлы",
"listUsers-inactiveNodes": "Неактивные узлы", "listUsers-inactiveNodes": "Неактивные узлы",
"ipInput-placeholder": "Введите IP", "ipInput-placeholder": "Введите IP",
"ipInput-error": "Введите корректный 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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import {ModalMap} from "../windowsType";
import {useModalContext} from "../../../store"; import {useModalContext} from "../../../store";
import styled from "styled-components"; import styled from "styled-components";
import { useSettingsStore } from '../../../store/settings/settingsStore'; import { useSettingsStore } from '../../../store/settings/settingsStore';
import {useTranslation} from "../../../utils/i18n/hooks";
const StyledTableList = styled.ul` const StyledTableList = styled.ul`
display: flex; display: flex;
@ -42,7 +43,7 @@ const CustomizeNodes = () => {
const updateSetting = useSettingsStore(state => state.updateSetting); const updateSetting = useSettingsStore(state => state.updateSetting);
const getSetting = useSettingsStore(state => state.getSetting); const getSetting = useSettingsStore(state => state.getSetting);
const initializeSettings = useSettingsStore(state => state.initializeSettings); const initializeSettings = useSettingsStore(state => state.initializeSettings);
const {t} = useTranslation('common');
const [dragIndex, setDragIndex] = useState<number | null>(null); const [dragIndex, setDragIndex] = useState<number | null>(null);
// Получаем текущие значения из хранилища // Получаем текущие значения из хранилища
@ -88,11 +89,22 @@ const CustomizeNodes = () => {
initializeSettings(); initializeSettings();
}, [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 ( return (
<ModalContainer style={{ height: '35vh', width: '80vw'}}> <ModalContainer style={{ height: '35vh', width: '80vw'}}>
<ModalTitle> <ModalTitle>
<h3>Кастомизация таблицы узлов подсети</h3> <h3>{t('customizeTable-title')}</h3>
</ModalTitle> </ModalTitle>
<ModalContent style={{ display: 'flex', height: "100%", gap: '24px', padding: 0 }}> <ModalContent style={{ display: 'flex', height: "100%", gap: '24px', padding: 0 }}>
@ -110,7 +122,7 @@ const CustomizeNodes = () => {
onDragEnd={handleDrop} onDragEnd={handleDrop}
onClick={() => handleSwitch(index)} onClick={() => handleSwitch(index)}
> >
{item} {getItemText(item)}
</StyledTableItem> </StyledTableItem>
))} ))}
</StyledTableList> </StyledTableList>
@ -120,7 +132,7 @@ const CustomizeNodes = () => {
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Назад' title={t('common-back')}
onClick={handleSave} onClick={handleSave}
/> />
</ButtonContainer> </ButtonContainer>

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import {MainButton} from "../UI/buttons/mainButton";
import React from 'react'; import React from 'react';
import {useModalContext} from "../../store"; import {useModalContext} from "../../store";
import {ModalMap} from "../modalwindows/windowsType"; import {ModalMap} from "../modalwindows/windowsType";
import {useTranslation} from "../../utils/i18n/hooks";
const SettingBlock = styled.div` const SettingBlock = styled.div`
display: flex; display: flex;
@ -89,6 +90,7 @@ interface SettingItemProps {
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => { const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation('common');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { openModal } = useModalContext(); const { openModal } = useModalContext();
@ -96,7 +98,35 @@ const SettingItem = React.memo(({ setting, value, onChange, onValidationError }:
if (setting.id === 'customize-nodes') { if (setting.id === 'customize-nodes') {
openModal(ModalMap['11']); openModal(ModalMap['11']);
} else if (setting.action) { } else if (setting.action) {
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();
} }
}; };
@ -106,19 +136,19 @@ const SettingItem = React.memo(({ setting, value, onChange, onValidationError }:
let newError: string | null = null; let newError: string | null = null;
if (inputValue.trim() === '') { if (inputValue.trim() === '') {
newError = 'Поле не может быть пустым'; newError = t('settings-fieldRequired');
onChange(null); onChange(null);
} }
else if (isNaN(Number(inputValue))) { else if (isNaN(Number(inputValue))) {
newError = 'Пожалуйста, введите число'; newError = t('settings-enterNumber');
} }
else { else {
const numValue = Number(inputValue); const numValue = Number(inputValue);
if (setting.min !== undefined && numValue < setting.min) { if (setting.min !== undefined && numValue < setting.min) {
newError = `Значение не может быть меньше ${setting.min}`; newError = t('settings-valueTooSmall') + setting.min;
} }
else if (setting.max !== undefined && numValue > setting.max) { else if (setting.max !== undefined && numValue > setting.max) {
newError = `Значение не может быть больше ${setting.max}`; newError = t('settings-valueTooLarge') + setting.max;
} }
if (!newError) { if (!newError) {

View file

@ -6,6 +6,7 @@ import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
import {SettingItem} from "./SettingItem"; import {SettingItem} from "./SettingItem";
import {IconComponent} from "../../const/buttons/icons"; import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names"; import {buttonNames} from "../../const/buttons/names";
import {useTranslation} from "../../utils/i18n/hooks";
interface Props { interface Props {
@ -70,9 +71,12 @@ export const Settings = ({ onClose }: Props) => {
const getSetting = useSettingsStore(state => state.getSetting); const getSetting = useSettingsStore(state => state.getSetting);
const resetToDefaults = useSettingsStore(state => state.resetToDefaults); const resetToDefaults = useSettingsStore(state => state.resetToDefaults);
const initializeSettings = useSettingsStore(state => state.initializeSettings); const initializeSettings = useSettingsStore(state => state.initializeSettings);
const updateSettingsConfig = useSettingsStore(state => state.updateSettingsConfig);
const theme = useTheme() const theme = useTheme()
const { t, language } = useTranslation('common');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({}); const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [currentLanguage, setCurrentLanguage] = useState(language);
// Проверка действий // Проверка действий
const handleValidationError = useCallback((fieldId: string, error: string | null) => { const handleValidationError = useCallback((fieldId: string, error: string | null) => {
@ -89,22 +93,33 @@ export const Settings = ({ onClose }: Props) => {
const settingsBySection = useMemo(() => { const settingsBySection = useMemo(() => {
return settingsConfig.reduce((acc, setting) => { return settingsConfig.reduce((acc, setting) => {
if (!acc[setting.sectionId]) { // Применяем переводы к настройкам
acc[setting.sectionId] = []; const translatedSetting = {
}
acc[setting.sectionId].push({
...setting, ...setting,
sectionId: t(setting.sectionId),
label: t(setting.label),
defaultValue: typeof setting.defaultValue === 'string' && setting.defaultValue.startsWith('settings-') ? t(setting.defaultValue) : setting.defaultValue,
options: setting.options ? setting.options.map((option: any) => ({
...option,
label: option.label.startsWith('settings-') ? t(option.label) : option.label
})) : setting.options,
value: settingsValues[setting.id] ?? setting.defaultValue value: settingsValues[setting.id] ?? setting.defaultValue
}); };
if (!acc[translatedSetting.sectionId]) {
acc[translatedSetting.sectionId] = [];
}
acc[translatedSetting.sectionId].push(translatedSetting);
return acc; return acc;
}, {} as Record<string, Setting[]>); }, {} as Record<string, Setting[]>);
}, [settingsConfig, settingsValues]); }, [settingsConfig, settingsValues, t]);
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]); const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
useEffect(() => { useEffect(() => {
updateSettingsConfig();
initializeSettings(); initializeSettings();
}, [initializeSettings]); }, [updateSettingsConfig, initializeSettings]);
useEffect(() => { useEffect(() => {
if (sections.length > 0 && !activeSection) { if (sections.length > 0 && !activeSection) {
@ -112,10 +127,15 @@ export const Settings = ({ onClose }: Props) => {
} }
}, [sections, activeSection, setActiveSection]); }, [sections, activeSection, setActiveSection]);
// Обновляем текущий язык при изменении
useEffect(() => {
setCurrentLanguage(language);
}, [language]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
const hasErrors = Object.values(fieldErrors).some(error => error?.trim()); const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
if (hasErrors) { if (hasErrors) {
alert('Исправьте ошибки перед сохранением'); alert(t('settings-fixErrors'));
return; return;
} }
@ -126,13 +146,24 @@ export const Settings = ({ onClose }: Props) => {
}); });
if (emptyRequiredFields.length > 0) { if (emptyRequiredFields.length > 0) {
alert('Заполните все обязательные поля'); alert(t('settings-fillRequired'));
return; return;
} }
// Проверяем, изменился ли язык
const newLanguage = pendingChanges['language'] ?? settingsValues['language'];
const languageChanged = newLanguage && newLanguage !== currentLanguage;
applyChanges(); applyChanges();
onClose(); onClose();
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose]);
// Если язык изменился, перезагружаем приложение
if (languageChanged) {
setTimeout(() => {
window.location.reload();
}, 100);
}
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose, t, currentLanguage]);
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
discardChanges(); discardChanges();
@ -141,14 +172,14 @@ export const Settings = ({ onClose }: Props) => {
}, [discardChanges, onClose]); }, [discardChanges, onClose]);
const handleReset = () => { const handleReset = () => {
if (window.confirm('Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?')) { if (window.confirm(t('settings-resetConfirm'))) {
resetToDefaults(); resetToDefaults();
} }
}; };
// Обработка нажатий клавиш // Обработка нажатий клавиш
useEffect(() => { useEffect(() => {
const handleKeyPress = (e: React.KeyboardEvent<HTMLElement>) => { const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter') if (e.key === 'Enter')
handleSave(); handleSave();
if (e.key === 'Escape') if (e.key === 'Escape')
@ -158,12 +189,13 @@ export const Settings = ({ onClose }: Props) => {
return () => { return () => {
document.removeEventListener('keydown', handleKeyPress); document.removeEventListener('keydown', handleKeyPress);
}; };
}, []); }, [handleSave, handleCancel]);
return ( return (
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}> <ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle> <ModalTitle>
<h3>Настройки</h3> <h3>{t('settings-title')}</h3>
<CloseBtn> <CloseBtn>
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} /> <IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
</CloseBtn> </CloseBtn>
@ -204,7 +236,7 @@ export const Settings = ({ onClose }: Props) => {
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Сбросить настройки' title={t('settings-resetSettings')}
onClick={handleReset} onClick={handleReset}
color={theme.offlineColor} color={theme.offlineColor}
hoverColor={theme.borderColor} hoverColor={theme.borderColor}
@ -213,7 +245,7 @@ export const Settings = ({ onClose }: Props) => {
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Отменить' title={t('settings-cancel')}
onClick={handleCancel} onClick={handleCancel}
color={theme.offlineColor} color={theme.offlineColor}
hoverColor={theme.borderColor} hoverColor={theme.borderColor}
@ -221,7 +253,7 @@ export const Settings = ({ onClose }: Props) => {
<MainButton <MainButton
type="button" type="button"
size="small" size="small"
title='Сохранить' title={t('settings-save')}
onClick={handleSave} onClick={handleSave}
/> />
</span> </span>

View file

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

View file

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

View file

@ -5,14 +5,14 @@ import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names"; import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType"; import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store"; import {useModalContext} from "../../../../../../store";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
import Pagination from "../../../../../UI/Pagination"; import Pagination from "../../../../../UI/Pagination";
import { import {
useDisabledColumns, useLanguage, useDisabledColumns,
useNameSetting, useNameSetting,
useOrdersColumns, useOrdersColumns,
usePaginateSetting usePaginateSetting
} from "../../../../../../hooks/useSettings"; } from "../../../../../../hooks/useSettings";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
interface TableScannerProps { interface TableScannerProps {
devices: DeviceUser[]; devices: DeviceUser[];
@ -71,16 +71,19 @@ const StatusBadge = styled.span<{ status:string }>`
` `
export const TableScanner = ({ devices }: TableScannerProps) => { export const TableScanner = ({ devices }: TableScannerProps) => {
const {t} = useTranslation('common')
const { openModal } = useModalContext(); const { openModal } = useModalContext();
const nameSetting = useNameSetting(); const nameSetting = useNameSetting();
const ordersColumns = useOrdersColumns(); const ordersColumns = useOrdersColumns();
const disabledColumns = useDisabledColumns(); const disabledColumns = useDisabledColumns();
const paginateSetting = usePaginateSetting(); const paginateSetting = usePaginateSetting();
const {t} = useTranslation('common');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting); const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
console.log(ordersColumns);
useEffect(() => { useEffect(() => {
setDevicesPerPage(paginateSetting); setDevicesPerPage(paginateSetting);
}, [paginateSetting]); }, [paginateSetting]);
@ -98,14 +101,14 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
.filter(column => !disabledColumns.includes(ordersColumns.indexOf(column))) .filter(column => !disabledColumns.includes(ordersColumns.indexOf(column)))
.map(column => { .map(column => {
switch(column) { switch(column) {
case t('tableScanner-online'): return 'status'; case 'Онлайн': return 'status';
case t('tableScanner-name'): return 'name'; case 'Имя': return 'name';
case t('tableScanner-vendor'): return 'vendor'; case 'Производитель': return 'vendor';
case t('tableScanner-ip'): return 'ip'; case 'IP адрес': return 'ip';
case t('tableScanner-ports'): return 'ports'; case 'Открытые порты': return 'ports';
case t('tableScanner-activity'): return 'activity'; case 'Последняя активность': return 'activity';
case t('tableScanner-note'): return 'note'; case 'Заметка': return 'note';
case t('tableScanner-action'): return 'action'; case 'Действие': return 'action';
default: return ''; default: return '';
} }
}) })
@ -136,7 +139,7 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
case 'status': case 'status':
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>; return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
case 'name': case 'name':
return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : t('tableScanner-unknown'))}</TableCell>; return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>;
case 'vendor': case 'vendor':
return <TableCell key="vendor">{item.vendor}</TableCell>; return <TableCell key="vendor">{item.vendor}</TableCell>;
case 'ip': case 'ip':
@ -168,12 +171,12 @@ export const TableScanner = ({ devices }: TableScannerProps) => {
{visibleColumns.map(column => { {visibleColumns.map(column => {
switch (column) { switch (column) {
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>; case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
case 'name': return <TableHeaderCell key="name">{t('tableScanner-name')}</TableHeaderCell>; case 'name': return <TableHeaderCell key="name">{t('settings-name')}</TableHeaderCell>;
case 'vendor': return <TableHeaderCell key="vendor">{t('tableScanner-vendor')}</TableHeaderCell>; case 'vendor': return <TableHeaderCell key="vendor">{t('settings-vendor')}</TableHeaderCell>;
case 'ip': return <TableHeaderCell key="ip">{t('tableScanner-ip')}</TableHeaderCell>; case 'ip': return <TableHeaderCell key="ip">{t('settings-ipAddress')}</TableHeaderCell>;
case 'ports': return <TableHeaderCell key="ports">{t('tableScanner-ports')}</TableHeaderCell>; case 'ports': return <TableHeaderCell key="ports">{t('settings-openPorts')}</TableHeaderCell>;
case 'activity': return <TableHeaderCell key="activity">{t('tableScanner-activity')}</TableHeaderCell>; case 'activity': return <TableHeaderCell key="activity">{t('settings-lastActivity')}</TableHeaderCell>;
case 'note': return <TableHeaderCell key="note">{t('tableScanner-note')}</TableHeaderCell>; case 'note': return <TableHeaderCell key="note">{t('settings-note')}</TableHeaderCell>;
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>; case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
default: return null; default: return null;
} }

View file

@ -5,6 +5,7 @@ import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner"; import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore"; import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store"; import {useIpStore} from "../../../../../store";
import {useTranslation} from "../../../../../utils/i18n/hooks";
// Анимации // Анимации
const ring1 = keyframes` const ring1 = keyframes`
@ -143,10 +144,11 @@ export function WifiScanner() {
const [scanInitiated, setScanInitiated] = useState(false); const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore(); const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null); const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const { t } = useTranslation('common');
const handleScan = async () => { const handleScan = async () => {
setWifiNetworks([]); setWifiNetworks([]);
setCurrentNetwork(null) setCurrentNetwork(null);
setLoading(true); setLoading(true);
setScanInitiated(true); setScanInitiated(true);
try { try {
@ -154,7 +156,7 @@ export function WifiScanner() {
const networks = res.data.access_points || []; const networks = res.data.access_points || [];
setWifiNetworks(networks); setWifiNetworks(networks);
} catch (err) { } catch (err) {
console.error("Ошибка сканирования сети:", err); console.error(t('wifiScanner-scanError'), err);
setWifiNetworks([]); setWifiNetworks([]);
} finally { } finally {
setLoading(false); setLoading(false);
@ -163,7 +165,7 @@ export function WifiScanner() {
return ( return (
<Wrapper scanInitiated={scanInitiated} loading={loading}> <Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование точек доступа Wi-Fi сети</TitleWrapper> <TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}> <UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span> <span className="center circel"></span>
@ -190,11 +192,15 @@ export function WifiScanner() {
{loading ? ( {loading ? (
<LoadingText> <LoadingText>
Сканирование точек доступа <ScanningText/> {t('wifiScanner-scanning')} <ScanningText/>
</LoadingText> </LoadingText>
) : ( ) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }> <ScanBtn
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'} onClick={handleScan}
disabled={loading}
scanInitiated={wifiNetworks.length > 0}
>
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
</ScanBtn> </ScanBtn>
)} )}
</Wrapper> </Wrapper>

View file

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

View file

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

View file

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

View file

@ -8,30 +8,30 @@ const createNetworkSettings = () => {
{ {
id: 'clean-network', id: 'clean-network',
type: 'button', type: 'button',
label: 'Очистить данные сессии', label: 'settings-clearSessionData',
sectionId: 'Точки доступа', sectionId: 'settings-sections-accessPoints',
defaultValue: 'Очистить', defaultValue: 'settings-clear',
action: async ()=>{ action: async ()=>{
try { try {
const res = await NetworkService.clearNetwork(ipAddress); const res = await NetworkService.clearNetwork(ipAddress);
console.log(res); console.log(res);
}catch (e){ }catch (e){
console.error(e); console.error(e);
alert('Данные не были удалены: '+ e) alert('settings-dataNotDeleted' + e)
} }
}, },
}, },
{ {
id: 'export-network', id: 'export-network',
type: 'button', type: 'button',
label: 'Экспорт данных точек доступа', label: 'settings-exportNetworkData',
sectionId: 'Точки доступа', sectionId: 'settings-sections-accessPoints',
defaultValue: 'Экспортировать', defaultValue: 'settings-export',
action: async () => { action: async () => {
try { try {
const response = await NetworkService.exportNetwork(ipAddress); const response = await NetworkService.exportNetwork(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({ const { filePath } = await window.electronAPI.showSaveDialog({
title: "Сохранить данные сетей", title: 'settings-saveNetworksTitle',
defaultPath: "network_export.json", defaultPath: "network_export.json",
filters: [{ name: "JSON", extensions: ["json"] }], filters: [{ name: "JSON", extensions: ["json"] }],
}); });
@ -41,10 +41,10 @@ const createNetworkSettings = () => {
filePath, filePath,
JSON.stringify(response.data, null, 2) JSON.stringify(response.data, null, 2)
); );
alert("Данные успешно экспортированы!"); alert('settings-exportSuccess');
} }
} catch (e: any) { } catch (e: any) {
alert("Ошибка экспорта: " + e.message); alert('settings-exportError' + e.message);
} }
}, },
@ -52,4 +52,4 @@ const createNetworkSettings = () => {
] as const; ] as const;
}; };
export const networkSettings = createNetworkSettings(); export { createNetworkSettings };

View file

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

View file

@ -1,8 +1,8 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import {nodesSettings} from "./config/nodesSettings"; import {createNodesSettings} from "./config/nodesSettings";
import { networkSettings } from './config/networkSettings'; import { createNetworkSettings } from './config/networkSettings';
import {appearanceSettings} from "./config/appearanceSettings"; import {createAppearanceSettings} from "./config/appearanceSettings";
type SettingValue = any; type SettingValue = any;
type SettingsDictionary = Record<string, SettingValue>; type SettingsDictionary = Record<string, SettingValue>;
@ -36,18 +36,13 @@ type SettingsState = {
resetToDefaults: () => void; resetToDefaults: () => void;
getSettingValue: <T = any>(id: string) => T; getSettingValue: <T = any>(id: string) => T;
getNumberSetting: (id: string, fallback?: number) => number; getNumberSetting: (id: string, fallback?: number) => number;
updateSettingsConfig: () => void;
}; };
const defaultSettings: Setting[] = [
...nodesSettings,
...networkSettings,
...appearanceSettings,
];
export const useSettingsStore = create<SettingsState>()( export const useSettingsStore = create<SettingsState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
settingsConfig: defaultSettings, settingsConfig: [],
settingsValues: {}, settingsValues: {},
activeSection: null, activeSection: null,
pendingChanges: {}, pendingChanges: {},
@ -90,6 +85,15 @@ export const useSettingsStore = create<SettingsState>()(
set({ settingsValues: newValues }); set({ settingsValues: newValues });
}, },
updateSettingsConfig: () => {
const defaultSettings: Setting[] = [
...createNodesSettings(),
...createNetworkSettings(),
...createAppearanceSettings(),
];
set({ settingsConfig: defaultSettings });
},
getSetting: (id) => { getSetting: (id) => {
const state = get(); const state = get();
const setting = state.settingsConfig.find(s => s.id === id); const setting = state.settingsConfig.find(s => s.id === id);

View file

@ -18,5 +18,5 @@ app.include_router(router)
network_service = NetworkService() network_service = NetworkService()
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt") #uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt")
#uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True)

View file

@ -0,0 +1,284 @@
{
"devices": [
{
"ip": "192.168.1.31",
"mac": "00:0c:29:a5:fb:9a",
"hostname": "OSGOS-OSOSGOS6",
"vendor": "VMware, Inc.",
"open_ports": [],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.1",
"mac": "dc:91:66:59:0c:1c",
"hostname": "_gateway",
"vendor": "Unknown",
"open_ports": [
"DNS (UDP & TCP)",
"HTTP",
"HTTPS"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.6",
"mac": "c0:83:c9:22:76:27",
"hostname": null,
"vendor": "Unknown",
"open_ports": [
"HTTP",
"HTTPS"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.27",
"mac": "54:16:51:32:d0:df",
"hostname": null,
"vendor": "Ruijie Networks Co.,LTD",
"open_ports": [
"HTTP"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.40",
"mac": "c0:83:c9:22:76:14",
"hostname": null,
"vendor": "Huawei Device Co., Ltd.",
"open_ports": [
"HTTP",
"HTTPS"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.41",
"mac": "22:b5:3c:8e:9d:7c",
"hostname": null,
"vendor": "Unknown",
"open_ports": [
"MySQL/MariaDB"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.45",
"mac": "38:1f:8d:d2:02:b5",
"hostname": null,
"vendor": "Tuya Smart Inc.",
"open_ports": [],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.50",
"mac": "38:1f:8d:d2:77:e5",
"hostname": null,
"vendor": "Tuya Smart Inc.",
"open_ports": [],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.52",
"mac": "38:1f:8d:d4:50:d5",
"hostname": null,
"vendor": "Tuya Smart Inc.",
"open_ports": [],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.64",
"mac": "dc:a6:32:13:68:e5",
"hostname": null,
"vendor": "Unknown",
"open_ports": [
"SSH",
"HTTP",
"HTTPS"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.108",
"mac": "b4:4c:3b:33:c1:fa",
"hostname": null,
"vendor": "Zhejiang Dahua Technology Co., Ltd.",
"open_ports": [
"HTTP",
"HTTPS"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.120",
"mac": "c0:39:5a:1a:9d:02",
"hostname": null,
"vendor": "Zhejiang Dahua Technology Co., Ltd.",
"open_ports": [
"HTTP"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.121",
"mac": "c0:39:5a:1a:a3:c0",
"hostname": null,
"vendor": "Zhejiang Dahua Technology Co., Ltd.",
"open_ports": [
"HTTP"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.122",
"mac": "c0:39:5a:1a:9c:c8",
"hostname": null,
"vendor": "Unknown",
"open_ports": [
"HTTP"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.123",
"mac": "c0:39:5a:1a:a3:a4",
"hostname": null,
"vendor": "Zhejiang Dahua Technology Co., Ltd.",
"open_ports": [
"HTTP"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.142",
"mac": "20:11:a2:68:02:0f",
"hostname": "OSGOS-OSOSGOS",
"vendor": "Unknown",
"open_ports": [
"SSH",
"HTTP",
"NetBIOS Session Service",
"SMB (Microsoft-DS)"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.148",
"mac": "dc:97:ba:3a:33:0b",
"hostname": "OSGOS-OSOSGOS9",
"vendor": "Unknown",
"open_ports": [
"SSH",
"NetBIOS Session Service",
"SMB (Microsoft-DS)",
"HTTP-Alt",
"SonarQube / PHP-FPM"
],
"status": "offline",
"last_seen": "07-07-2025, 15:31",
"note": null
},
{
"ip": "192.168.1.168",
"mac": "e2:ec:26:f8:6b:25",
"hostname": null,
"vendor": "Unknown",
"open_ports": [],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.253",
"mac": "5c:1b:f4:97:eb:56",
"hostname": null,
"vendor": "Unknown",
"open_ports": [
"SSH",
"DNS (UDP & TCP)",
"SMB (Microsoft-DS)",
"VNC"
],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
},
{
"ip": "192.168.1.9",
"mac": "5e:28:1f:e5:7d:18",
"hostname": null,
"vendor": "Unknown",
"open_ports": [],
"status": "offline",
"last_seen": "07-07-2025, 15:31",
"note": null
},
{
"ip": "192.168.1.10",
"mac": "f6:98:16:d2:a1:0f",
"hostname": null,
"vendor": "Unknown",
"open_ports": [
"SSH",
"DNS (UDP & TCP)",
"SMB (Microsoft-DS)",
"VNC"
],
"status": "offline",
"last_seen": "07-07-2025, 15:31",
"note": null
},
{
"ip": "192.168.1.69",
"mac": "52:c4:40:b1:21:4c",
"hostname": null,
"vendor": "Unknown",
"open_ports": [],
"status": "offline",
"last_seen": "07-07-2025, 15:31",
"note": null
},
{
"ip": "192.168.1.30",
"mac": "10:ec:81:fe:10:39",
"hostname": null,
"vendor": "Samsung Electronics Co.,Ltd",
"open_ports": [],
"status": "online",
"last_seen": "07-07-2025, 16:22",
"note": null
}
]
}