diff --git a/app/src/main/java/ru/kirillius/pf/sdn/App.java b/app/src/main/java/ru/kirillius/pf/sdn/App.java index 865b857..0fa7f4f 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/App.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java @@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn; import lombok.Getter; import lombok.SneakyThrows; +import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.pf.sdn.External.API.Components.FRR; import ru.kirillius.pf.sdn.External.API.Components.OVPN; import ru.kirillius.pf.sdn.External.API.Components.TDNS; @@ -9,7 +10,6 @@ import ru.kirillius.pf.sdn.External.API.HEInfoProvider; import ru.kirillius.pf.sdn.core.Auth.AuthManager; import ru.kirillius.pf.sdn.core.Auth.TokenStorage; import ru.kirillius.pf.sdn.core.*; -import ru.kirillius.pf.sdn.core.Component; import ru.kirillius.pf.sdn.core.Networking.ASInfoService; import ru.kirillius.pf.sdn.core.Networking.NetworkManager; import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager; @@ -21,6 +21,7 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -41,7 +42,7 @@ public class App implements Context, Closeable { @Getter private final NetworkManager networkManager; @Getter - private Config config; + private volatile Config config; @Getter private final AuthManager authManager; @Getter @@ -119,19 +120,29 @@ public class App implements Context, Closeable { return List.of(FRR.class, OVPN.class, TDNS.class); } + private void unloadComponent(Component component) { + SystemLogger.message("Unloading component: " + component.getClass().getSimpleName(), CTX); + try { + component.close(); + } catch (IOException e) { + SystemLogger.error("Error on component unload", CTX, e); + } finally { + loadedComponents.remove(component); + } + } + + private void loadComponent(Class> componentClass) { + SystemLogger.message("Loading component: " + componentClass.getSimpleName(), CTX); + var plugin = Component.loadPlugin(componentClass, this); + loadedComponents.add(plugin); + } + public void initComponents() { var enabledPlugins = config.getEnabledComponents(); (List.copyOf(loadedComponents)).forEach(plugin -> { if (!enabledPlugins.contains(plugin.getClass())) { - SystemLogger.message("Unloading plugin: " + plugin.getClass().getSimpleName(), CTX); - try { - plugin.close(); - } catch (IOException e) { - SystemLogger.error("Error on plugin unload", CTX, e); - } finally { - loadedComponents.remove(plugin); - } + unloadComponent(plugin); } }); var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList(); @@ -139,12 +150,31 @@ public class App implements Context, Closeable { if (loadedClasses.contains(pluginClass)) { return; } - SystemLogger.message("Loading plugin: " + pluginClass.getSimpleName(), CTX); - var plugin = Component.loadPlugin(pluginClass, this); - loadedComponents.add(plugin); + loadComponent(pluginClass); }); } + @Override + public void reloadComponents(Class>... classes) { + Arrays.stream(classes) + .forEach(componentClass -> { + loadedComponents.stream() + .filter(component -> componentClass.equals(component.getClass())) + .findFirst().ifPresent(this::unloadComponent); + loadComponent(componentClass); + } + ); + + } + + @Override + public JSONRPCServlet getRPC() { + if (server == null) { + return null; + } + return server.getJSONRPC(); + } + private void subscribe() { var eventsHandler = getEventsHandler(); eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> { @@ -158,7 +188,7 @@ public class App implements Context, Closeable { } @Override - public Component getPluginInstance(Class> pluginClass) { + public Component getComponentInstance(Class> pluginClass) { return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null); } diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java index 21b08ea..e382c6f 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java @@ -24,17 +24,24 @@ public final class OVPN extends AbstractComponent { public OVPN(Context context) { super(context); + var RPC = context.getRPC(); + if (RPC != null) { + RPC.addTargetInstance(OVPN.class, this); + subscription = null; + return; + } subscription = context.getEventsHandler().getRPCInitEvent() .add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this)); } @JRPCMethod @ProtectedMethod - public void restartSystemService() { + public String restartSystemService() { try (var shell = new ShellExecutor(config.shellConfig)) { - shell.executeCommand(new String[]{config.restartCommand}); + return shell.executeCommand(new String[]{config.restartCommand}); } catch (IOException e) { SystemLogger.error("Error when trying to restart OVPN", CTX, e); + return e.toString(); } } @@ -53,8 +60,10 @@ public final class OVPN extends AbstractComponent { @Override - public void close() throws IOException { - context.getEventsHandler().getRPCInitEvent().remove(subscription); + public void close() { + if (subscription != null) { + context.getEventsHandler().getRPCInitEvent().remove(subscription); + } } @JSONSerializable @@ -62,11 +71,11 @@ public final class OVPN extends AbstractComponent { @Getter @Setter @JSONProperty - private ShellExecutor.Config shellConfig = new ShellExecutor.Config(); + private volatile ShellExecutor.Config shellConfig = new ShellExecutor.Config(); @Getter @Setter @JSONProperty - private String restartCommand = "rc-service openvpn restart"; + private volatile String restartCommand = "rc-service openvpn restart"; } } diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java index c76a4a2..99b1013 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/ShellExecutor.java @@ -85,23 +85,23 @@ public class ShellExecutor implements Closeable { @Getter @Setter @JSONProperty - private boolean useSSH = false; + private volatile boolean useSSH = false; @Getter @Setter @JSONProperty - private String host = "127.0.0.1"; + private volatile String host = "127.0.0.1"; @Getter @Setter @JSONProperty - private int port = 22; + private volatile int port = 22; @Getter @Setter @JSONProperty - private String username = "root"; + private volatile String username = "root"; @Getter @Setter @JSONProperty - private String password = "securepassword"; + private volatile String password = "securepassword"; @Override public String toString() { diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java b/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java index a5b6079..8d64354 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/HTTPServer.java @@ -1,6 +1,7 @@ package ru.kirillius.pf.sdn.web; +import lombok.Getter; import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.server.Server; @@ -34,6 +35,8 @@ public class HTTPServer extends Server { private final static Set> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class); + @Getter + private JSONRPCServlet JSONRPC = new JSONRPCServlet(); public HTTPServer(Context appContext) { var config = appContext.getConfig(); @@ -46,7 +49,7 @@ public class HTTPServer extends Server { this.addConnector(connector); var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS); - var JSONRPC = new JSONRPCServlet(); + servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH); var holder = servletContext.addServlet(DefaultServlet.class, "/"); try { diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java index 7df3837..b94e989 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java @@ -93,5 +93,8 @@ public class System implements RPC { var cls = (Class) Class.forName(componentName); var configClass = Component.getConfigClass(cls); context.getConfig().getComponentsConfig().setConfig(cls, JSONUtility.deserializeStructure(config, configClass)); + context.reloadComponents(cls); + Object config1 = context.getConfig().getComponentsConfig().getConfig(cls); + return; } } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java index ce264bc..73c18b0 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java @@ -24,85 +24,85 @@ public class Config { @Getter @Setter @JSONProperty - private int updateSubscriptionsInterval = 6; + private volatile int updateSubscriptionsInterval = 6; @Getter @Setter @JSONProperty - private boolean cachingAS = true; + private volatile boolean cachingAS = true; @Getter @Setter @JSONProperty - private int updateASInterval = 12; + private volatile int updateASInterval = 12; @Getter - private File loadedConfigFile = null; + private volatile File loadedConfigFile = null; @Setter @Getter @JSONProperty - private String host = "0.0.0.0"; + private volatile String host = "0.0.0.0"; @Setter @Getter @JSONProperty - private File cacheDirectory = new File("./.cache"); + private volatile File cacheDirectory = new File("./.cache"); @Setter @Getter @JSONArrayProperty(type = RepositoryConfig.class) - private List subscriptions = Collections.emptyList(); + private volatile List subscriptions = Collections.emptyList(); @Setter @Getter @JSONArrayProperty(type = String.class) - private List subscribedResources = Collections.emptyList(); + private volatile List subscribedResources = Collections.emptyList(); @Setter @Getter @JSONProperty(required = false) - private ComponentConfigStorage componentsConfig = new ComponentConfigStorage(); + private volatile ComponentConfigStorage componentsConfig = new ComponentConfigStorage(); @Setter @Getter @JSONArrayProperty(type = Class.class, required = false) - private List>> enabledComponents = new ArrayList<>(); + private volatile List>> enabledComponents = new ArrayList<>(); @Setter @Getter @JSONProperty - private String passwordSalt = UUID.randomUUID().toString(); + private volatile String passwordSalt = UUID.randomUUID().toString(); @Setter @Getter @JSONProperty - private String passwordHash = ""; + private volatile String passwordHash = ""; @Setter @Getter @JSONProperty - private int httpPort = 8081; + private volatile int httpPort = 8081; @Setter @Getter @JSONProperty - private boolean mergeSubnets = true; + private volatile boolean mergeSubnets = true; @Setter @Getter @JSONProperty - private int mergeSubnetsWithUsage = 51; + private volatile int mergeSubnetsWithUsage = 51; @Setter @Getter @JSONProperty - private NetworkResourceBundle customResources = new NetworkResourceBundle(); + private volatile NetworkResourceBundle customResources = new NetworkResourceBundle(); @Setter @Getter @JSONProperty - private NetworkResourceBundle filteredResources = new NetworkResourceBundle(); + private volatile NetworkResourceBundle filteredResources = new NetworkResourceBundle(); public static void store(Config config, File file) throws IOException { try (var fileInputStream = new FileOutputStream(file)) { diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java index 5bb0cf0..292c32c 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Context.java @@ -1,5 +1,6 @@ package ru.kirillius.pf.sdn.core; +import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.pf.sdn.core.Auth.AuthManager; import ru.kirillius.pf.sdn.core.Auth.TokenStorage; import ru.kirillius.pf.sdn.core.Networking.ASInfoService; @@ -23,7 +24,7 @@ public interface Context { UpdateManager getUpdateManager(); - Component getPluginInstance(Class> pluginClass); + Component getComponentInstance(Class> pluginClass); void triggerRestart(); @@ -31,7 +32,11 @@ public interface Context { TokenStorage getTokenStorage(); - void initComponents(); + void initComponents(); - Collection>> getComponentClasses(); + void reloadComponents(Class>... classes); + + JSONRPCServlet getRPC(); + + Collection>> getComponentClasses(); } diff --git a/webui/src/modules/ui.js b/webui/src/modules/ui.js index 01e2c79..f965f0a 100644 --- a/webui/src/modules/ui.js +++ b/webui/src/modules/ui.js @@ -10,8 +10,10 @@ export function renderLoginForm() {

Авторизация

-
-
+
@@ -73,12 +75,14 @@ export function renderDashboard() {
-
+
`; diff --git a/webui/src/pages/APITokens.js b/webui/src/pages/APITokens.js index f83613b..1fda03b 100644 --- a/webui/src/pages/APITokens.js +++ b/webui/src/pages/APITokens.js @@ -1,5 +1,158 @@ -// src/pages/APITokens.js +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; -import { APITokensPage } from '../ui/api_tokens.js'; +let tokens = []; -export const APITokens = APITokensPage; \ No newline at end of file +async function loadTokens() { + try { + tokens = await JSONRPC.Auth.listTokens(); + tokens = tokens || []; + return true; + } catch (e) { + console.error('Ошибка при загрузке токенов:', e); + tokens = []; + return false; + } +} + +function renderTokenList() { + const $listContainer = $('#api-token-container'); + + const createButtonHtml = ` + + + `; + + if (tokens.length === 0) { + $listContainer.html(createButtonHtml + '

Активных API токенов не найдено.

'); + attachEventHandlers(); + return; + } + + const listHtml = tokens.map(item => { + const displayToken = item.token ? + `${item.token.substring(0, 8)}...${item.token.substring(item.token.length - 4)}` : + 'Неизвестный токен'; + + return ` +
  • +
    ${item.description}
    +
    + Ключ: ${displayToken} + +
    +
  • + `; + }).join(''); + + const html = ` + ${createButtonHtml} +
      ${listHtml}
    + `; + + $listContainer.html(html); + attachEventHandlers(); +} + +function showCreateTokenModal() { + const description = prompt('Введите описание для нового API токена (например, "Токен для Telegram бота"):'); + + if (description === null) { + return; + } + + if (description.trim() === '') { + alert('Описание токена не может быть пустым.'); + return; + } + + createToken(description.trim()); +} + +async function createToken(description) { + const $btn = $('#create-token-btn'); + const $message = $('#token-status-message'); + + $message.hide().removeClass('error-message').addClass('success-message'); + $btn.prop('disabled', true).text('Создание...'); + + try { + const newTokenValue = await JSONRPC.Auth.createAPIToken(description); + + if (!newTokenValue) { + throw new Error('Сервер не вернул токен.'); + } + + prompt('ВАЖНО: Ваш новый токен API. Сохраните его, он больше не будет показан!', newTokenValue); + + await loadTokens(); + renderTokenList(); + + $message.text(`Токен "${description}" успешно создан.`).show(); + } catch (e) { + console.error('Ошибка создания токена:', e); + $message.removeClass('success-message').addClass('error-message').text('Ошибка при создании токена.').show(); + } finally { + $btn.prop('disabled', false).text('Создать токен API'); + setTimeout(() => $message.fadeOut(), 8000); + } +} + +async function deleteToken(token) { + if (!confirm('Вы уверены, что хотите удалить этот API токен?')) { + return; + } + + const $item = $(`li.token-item[data-full-token="${token}"]`); + $item.addClass('deleting').css('opacity', 0.5); + + try { + await JSONRPC.Auth.removeToken(token); + tokens = tokens.filter(item => item.token !== token); + renderTokenList(); + + const $message = $('#token-status-message'); + $message.text('Токен успешно удален.').addClass('success-message').show(); + } catch (e) { + console.error('Ошибка удаления токена:', e); + $item.removeClass('deleting').css('opacity', 1); + const $message = $('#token-status-message'); + $message.removeClass('success-message').addClass('error-message').text('Ошибка при удалении токена.').show(); + } +} + +function attachEventHandlers() { + $('#create-token-btn').off('click').on('click', showCreateTokenModal); + + $('#api-token-container').off('click', '.delete-token-btn').on('click', '.delete-token-btn', function () { + const tokenToDelete = $(this).data('token'); + if (tokenToDelete) { + deleteToken(tokenToDelete); + } + }); +} + +export const APITokens = { + render: () => ` +

    API Токены

    +
    +

    Загрузка токенов...

    +
    + `, + mount: async () => { + const success = await loadTokens(); + if (success) { + renderTokenList(); + } else { + $('#api-token-container').html('

    Не удалось загрузить токены. Проверьте соединение с сервером.

    '); + } + }, + unmount: () => { + tokens = []; + $('#api-token-container').off(); + } +}; \ No newline at end of file diff --git a/webui/src/pages/Components.js b/webui/src/pages/Components.js index 5eba55e..f3ef78b 100644 --- a/webui/src/pages/Components.js +++ b/webui/src/pages/Components.js @@ -1,5 +1,107 @@ -// src/pages/Components.js +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; -import { ComponentsPage } from '../ui/components.js'; +let availableComponents = []; +let enabledComponents = []; -export const Components = ComponentsPage; \ No newline at end of file +function getShortName(fullName) { + const lastDotIndex = fullName.lastIndexOf('.'); + if (lastDotIndex > -1 && lastDotIndex < fullName.length - 1) { + return fullName.substring(lastDotIndex + 1); + } + return fullName; +} + +async function loadComponentData() { + try { + enabledComponents = await JSONRPC.System.getEnabledComponents(); + availableComponents = await JSONRPC.System.getAvailableComponents(); + availableComponents.sort(); + } catch (e) { + console.error('Ошибка при загрузке данных компонентов:', e); + availableComponents = []; + enabledComponents = []; + return false; + } + return true; +} + +function renderComponentList() { + const $listContainer = $('#component-list-container'); + + if (availableComponents.length === 0) { + $listContainer.html('

    Нет доступных системных компонентов.

    '); + return; + } + + const listHtml = availableComponents.map(fullName => { + const shortName = getShortName(fullName); + const isChecked = enabledComponents.includes(fullName); + + return ` +
  • + +
  • + `; + }).join(''); + + const html = ` +
      ${listHtml}
    + + + `; + + $listContainer.html(html); + attachEventHandlers(); +} + +function attachEventHandlers() { + $('#save-components-btn').on('click', async function () { + const $btn = $(this); + const $message = $('#component-status-message'); + $message.hide().removeClass('success-message').addClass('error-message'); + + const newEnabledComponents = $('.component-checkbox:checked').map(function () { + return $(this).val(); + }).get(); + + $btn.prop('disabled', true).text('Сохранение...'); + + try { + await JSONRPC.System.setEnabledComponents(newEnabledComponents); + enabledComponents = newEnabledComponents; + $message.text('Настройки компонентов успешно сохранены!').addClass('success-message').show(); + } catch (e) { + console.error('Ошибка сохранения компонентов:', e); + $message.text('Ошибка при сохранении компонентов.').show(); + } finally { + $btn.prop('disabled', false).text('Сохранить'); + setTimeout(() => $message.fadeOut(), 5000); + } + }); +} + +export const Components = { + render: () => ` +

    Управление Компонентами

    +
    +

    Загрузка доступных компонентов...

    +
    + `, + mount: async () => { + const success = await loadComponentData(); + if (success) { + renderComponentList(); + } else { + $('#component-list-container').html('

    Не удалось загрузить данные. Проверьте соединение с сервером.

    '); + } + }, + unmount: () => { + availableComponents = []; + enabledComponents = []; + } +}; \ No newline at end of file diff --git a/webui/src/pages/OVPN.js b/webui/src/pages/OVPN.js index 45d4b40..6514877 100644 --- a/webui/src/pages/OVPN.js +++ b/webui/src/pages/OVPN.js @@ -1,5 +1,237 @@ -// src/pages/OVPN.js +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; -import { OVPNConfigPage } from '../ui/ovpn.js'; +const OVPN_COMPONENT_NAME = 'ru.kirillius.pf.sdn.External.API.Components.OVPN'; +const DEFAULT_PORT = 22; -export const OVPNConfig = OVPNConfigPage; \ No newline at end of file +const FIELD_IDS = { + host: 'ovpn-host', + port: 'ovpn-port', + username: 'ovpn-username', + password: 'ovpn-password', + useSSH: 'ovpn-use-ssh', + restartCommand: 'ovpn-restart-command', + saveButton: 'save-ovpn-btn', + restartButton: 'restart-ovpn-btn', + status: 'ovpn-status-message', + container: 'ovpn-config-container' +}; + +const SELECTORS = { + container: `#${FIELD_IDS.container}`, + host: `#${FIELD_IDS.host}`, + port: `#${FIELD_IDS.port}`, + username: `#${FIELD_IDS.username}`, + password: `#${FIELD_IDS.password}`, + useSSH: `#${FIELD_IDS.useSSH}`, + restartCommand: `#${FIELD_IDS.restartCommand}`, + saveButton: `#${FIELD_IDS.saveButton}`, + restartButton: `#${FIELD_IDS.restartButton}`, + status: `#${FIELD_IDS.status}` +}; + +let currentConfig = {}; +let statusTimeoutId = null; + +const getStatusElement = () => $(SELECTORS.status); + +function clearStatus() { + const $status = getStatusElement(); + if (!$status.length) { + return; + } + if (statusTimeoutId) { + clearTimeout(statusTimeoutId); + statusTimeoutId = null; + } + $status.stop(true, true).hide().text('').removeClass('success-message error-message'); +} + +function updateStatus(message, type) { + const $status = getStatusElement(); + if (!$status.length) { + return; + } + if (statusTimeoutId) { + clearTimeout(statusTimeoutId); + } + $status + .removeClass('success-message error-message') + .addClass(type === 'success' ? 'success-message' : 'error-message') + .text(message) + .show(); + + statusTimeoutId = window.setTimeout(() => { + $status.fadeOut(); + }, 5000); +} + +async function runAction($button, pendingText, action, messages) { + if (!$button.length) { + return; + } + + const originalText = $button.text(); + $button.prop('disabled', true).text(pendingText); + clearStatus(); + + try { + const result = await action(); + if (messages?.success) { + const successMessage = typeof messages.success === 'function' + ? messages.success(result) + : messages.success; + if (successMessage) { + updateStatus(successMessage, 'success'); + } + } + } catch (error) { + console.error(messages?.log || 'Ошибка выполнения действия OVPN:', error); + if (messages?.error) { + updateStatus(messages.error, 'error'); + } + } finally { + $button.prop('disabled', false).text(originalText); + } +} + +async function loadConfig() { + try { + const fullConfig = await JSONRPC.System.getComponentConfig(OVPN_COMPONENT_NAME); + currentConfig = fullConfig || {}; + return true; + } catch (error) { + console.error('Ошибка при загрузке конфига OVPN:', error); + return false; + } +} + +function renderOVPNForm() { + const $container = $(SELECTORS.container); + const shellConfig = currentConfig.shellConfig || {}; + + $container.html(` +
    +

    Конфигурация Shell (ShellConfig)

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +

    Перезапуск сервиса

    +
    + + + Команда, которая будет выполнена через Shell для перезапуска сервиса OVPN. +
    + +
    + + +
    + +
    + `); + + attachEventHandlers(); +} + +function collectConfigFromForm() { + const shellConfig = currentConfig.shellConfig || {}; + const newPassword = $(SELECTORS.password).val(); + const useSSH = $(SELECTORS.useSSH).prop('checked'); + + return { + shellConfig: { + useSSH, + host: $(SELECTORS.host).val(), + port: parseInt($(SELECTORS.port).val(), 10) || DEFAULT_PORT, + username: $(SELECTORS.username).val(), + password: newPassword ? newPassword : (shellConfig.password || ''), + }, + restartCommand: $(SELECTORS.restartCommand).val() + }; +} + +async function handleSave() { + const $button = $(SELECTORS.saveButton); + await runAction($button, 'Сохранение...', async () => { + const newConfig = collectConfigFromForm(); + await JSONRPC.System.setComponentConfig(OVPN_COMPONENT_NAME, newConfig); + currentConfig = newConfig; + }, { + success: 'Конфигурация OVPN успешно сохранена.', + error: 'Ошибка при сохранении конфигурации.', + log: 'Ошибка сохранения конфига OVPN' + }); +} + +async function handleRestart() { + const $button = $(SELECTORS.restartButton); + await runAction($button, 'Перезапуск...', async () => { + return JSONRPC.OVPN.restartSystemService(); + }, { + success: (result) => result || 'Сервис OVPN успешно перезапущен.', + error: 'Ошибка при перезапуске сервиса.', + log: 'Ошибка перезапуска OVPN' + }); +} + +function attachEventHandlers() { + $(SELECTORS.saveButton).off('click').on('click', handleSave); + $(SELECTORS.restartButton).off('click').on('click', handleRestart); + $(SELECTORS.useSSH).off('change').on('change', toggleSSHFields); + toggleSSHFields(); +} + +function detachEventHandlers() { + $(SELECTORS.saveButton).off('click'); + $(SELECTORS.restartButton).off('click'); + $(SELECTORS.useSSH).off('change'); +} + +function toggleSSHFields() { + const enabled = $(SELECTORS.useSSH).prop('checked'); + $(SELECTORS.host).prop('disabled', !enabled); + $(SELECTORS.port).prop('disabled', !enabled); + $(SELECTORS.username).prop('disabled', !enabled); + $(SELECTORS.password).prop('disabled', !enabled); +} + +export const OVPNConfig = { + render: () => ` +

    Настройка OVPN

    +
    +

    Загрузка конфигурации...

    +
    + `, + mount: async () => { + const success = await loadConfig(); + if (success) { + renderOVPNForm(); + } else { + $(SELECTORS.container).html('

    Не удалось загрузить конфигурацию OVPN.

    '); + } + }, + unmount: () => { + detachEventHandlers(); + clearStatus(); + currentConfig = {}; + } +}; \ No newline at end of file diff --git a/webui/src/pages/Statistics.js b/webui/src/pages/Statistics.js index cb14b39..60d1eb2 100644 --- a/webui/src/pages/Statistics.js +++ b/webui/src/pages/Statistics.js @@ -1,22 +1,179 @@ -// src/pages/Statistics.js +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; -import { renderStatisticsPage, stopPolling } from '../ui/statistics.js'; +const POLLING_INTERVAL = 5000; +let updateInterval = null; -export const StatisticsPage = { - render: () => { - return ` -

    Статистика

    -
    - Загрузка данных... -
    - `; - }, - mount: () => { - // Вызывает renderStatisticsPage, который запускает опрос - renderStatisticsPage(); - }, - // 🔥 ВСТРОЕННАЯ ФУНКЦИЯ UNMOUNT: Роутер использует StatisticsPage.unmount - unmount: stopPolling +const $content = () => $('#statistics-content'); + +const createStatusCard = (title, status, buttonId, buttonLabel, isUpdating, resourceStatsHtml = '') => { + const statusClass = status === 'Обновляется' || status === 'Обнаружено' ? 'text-yellow-500' : 'text-green-500'; + + return ` +
    +

    ${title}

    +

    + Статус: ${status} +

    + + ${resourceStatsHtml} +
    + `; }; -// 🔥 УДАЛЕН ЭКСПОРТ { stopPolling }; из этого файла. \ No newline at end of file +const createResourceStatsHtml = (resources) => { + return ` +
    +

    Ресурсы:

    +
    + ${createResourceBadge("Домены", resources.domains.length)} + ${createResourceBadge("ASN", resources.ASN.length)} + ${createResourceBadge("Подсети", resources.subnets.length)} +
    +
    + `; +}; + +const createResourceBadge = (title, count) => { + return ` +
    + ${count} + ${title} +
    + `; +}; + +async function getSubscriptionCount() { + try { + const subscribedResources = await JSONRPC.SubscriptionManager.getSubscribedResources(); + return (subscribedResources && subscribedResources.length) || 0; + } catch (e) { + console.error('Ошибка при получении subscribedResources:', e); + return 0; + } +} + +async function getNetworkResources() { + try { + const resources = await JSONRPC.NetworkManager.getOutputResources(); + return { + domains: resources.domains || [], + ASN: resources.ASN || [], + subnets: resources.subnets || [] + }; + } catch (e) { + console.error('Ошибка при получении ресурсов:', e); + return { domains: [], ASN: [], subnets: [] }; + } +} + +async function renderSystemAndManagerStatus() { + const dataHtml = []; + + const hasUpdates = await JSONRPC.System.hasUpdates(); + const isSubUpdating = await JSONRPC.SubscriptionManager.isUpdating(); + const isNetUpdating = await JSONRPC.NetworkManager.isUpdating(); + const subCount = await getSubscriptionCount(); + const networkResources = await getNetworkResources(); + + dataHtml.push(createStatusCard( + 'Обновление ПО', + hasUpdates ? 'Обнаружено' : 'Нет обновлений', + 'update-software-btn', + 'Проверить', + false + )); + + const subStatsHtml = `
    +

    Подписки:

    +
    + ${createResourceBadge("Активно", subCount)} +
    +
    `; + dataHtml.push(createStatusCard( + 'Менеджер Подписок', + isSubUpdating ? 'Обновляется' : 'Активен', + 'update-sub-btn', + 'Обновить', + isSubUpdating, + subStatsHtml + )); + + const netResourcesHtml = createResourceStatsHtml(networkResources); + dataHtml.push(createStatusCard( + 'Менеджер Сетей', + isNetUpdating ? 'Обновляется' : 'Активен', + 'update-net-btn', + 'Обновить', + isNetUpdating, + netResourcesHtml + )); + + $content().html(`
    ${dataHtml.join('')}
    `); + + attachEventHandlers(); +} + +function stopPolling() { + if (updateInterval) { + console.log('clear interval'); + clearInterval(updateInterval); + updateInterval = null; + } +} + +function attachEventHandlers() { + $('#update-sub-btn').off('click').on('click', async function () { + const $btn = $(this); + $btn.prop('disabled', true).text('Обновление...'); + + try { + await JSONRPC.SubscriptionManager.triggerUpdate(); + alert('Обновление подписок запущено!'); + renderSystemAndManagerStatus(); + } catch (e) { + alert('Ошибка при запуске обновления подписок!'); + $btn.prop('disabled', false).text('Обновить'); + } + }); + + $('#update-net-btn').off('click').on('click', async function () { + const $btn = $(this); + $btn.prop('disabled', true).text('Обновление...'); + + try { + await JSONRPC.NetworkManager.triggerUpdate(true); + alert('Обновление сетей запущено!'); + renderSystemAndManagerStatus(); + } catch (e) { + alert('Ошибка при запуске обновления сетей!'); + $btn.prop('disabled', false).text('Обновить'); + } + }); + + $('#update-software-btn').off('click').on('click', function () { + alert('Проверка обновлений ПО запущена...'); + }); +} + +async function mountStatisticsPage() { + stopPolling(); + await renderSystemAndManagerStatus(); + console.log('start interval'); + updateInterval = setInterval(renderSystemAndManagerStatus, POLLING_INTERVAL); +} + +export const StatisticsPage = { + render: () => ` +

    Статистика

    +
    + Загрузка данных... +
    + `, + mount: () => { + mountStatisticsPage(); + }, + unmount: stopPolling +}; \ No newline at end of file diff --git a/webui/src/pages/Subscriptions.js b/webui/src/pages/Subscriptions.js index 7fda284..aec1f9e 100644 --- a/webui/src/pages/Subscriptions.js +++ b/webui/src/pages/Subscriptions.js @@ -1,6 +1,110 @@ -// src/pages/Subscriptions.js +import $ from 'jquery'; +import { JSONRPC } from '@/json-rpc.js'; -import { SubscriptionsPage } from '../ui/subscriptions.js'; +let availableResources = {}; +let subscribedKeys = []; -// Просто реэкспортируем объект для роутера -export const Subscriptions = SubscriptionsPage; \ No newline at end of file +async function loadSubscriptionData() { + try { + subscribedKeys = await JSONRPC.SubscriptionManager.getSubscribedResources(); + availableResources = await JSONRPC.SubscriptionManager.getAvailableResources(); + } catch (e) { + console.error('Ошибка при загрузке данных подписок:', e); + availableResources = {}; + subscribedKeys = []; + return false; + } + return true; +} + +function renderSubscriptionList() { + const $listContainer = $('#subscription-list-container'); + const availableKeys = Object.keys(availableResources).sort(); + + if (availableKeys.length === 0) { + $listContainer.html('

    Нет доступных ресурсов для подписки.

    '); + return; + } + + const listHtml = availableKeys.map(key => { + const details = availableResources[key]; + const isChecked = subscribedKeys.includes(key); + const description = details.description || 'Нет описания'; + const detailText = ` + Доменов: ${details.domains ? details.domains.length : 0}, + ASN: ${details.ASN ? details.ASN.length : 0}, + Подсетей: ${details.subnets ? details.subnets.length : 0} + `; + + return ` +
  • + +
  • + `; + }).join(''); + + const html = ` +
      ${listHtml}
    + + + `; + + $listContainer.html(html); + attachEventHandlers(); +} + +function attachEventHandlers() { + $('#save-subscriptions-btn').on('click', async function () { + const $btn = $(this); + const $message = $('#subscription-status-message'); + $message.hide().removeClass('success-message').addClass('error-message'); + + const newSubscribedKeys = $('.subscription-checkbox:checked').map(function () { + return $(this).val(); + }).get(); + + $btn.prop('disabled', true).text('Сохранение...'); + + try { + await JSONRPC.SubscriptionManager.setSubscribedResources(newSubscribedKeys); + subscribedKeys = newSubscribedKeys; + $message.text('Настройки подписок успешно сохранены!').addClass('success-message').show(); + } catch (e) { + console.error('Ошибка сохранения подписок:', e); + $message.text('Ошибка при сохранении подписок.').show(); + } finally { + $btn.prop('disabled', false).text('Сохранить'); + setTimeout(() => $message.fadeOut(), 5000); + } + }); +} + +export const Subscriptions = { + render: () => ` +

    Управление Подписками

    +
    +

    Загрузка доступных ресурсов...

    +
    + `, + mount: async () => { + const success = await loadSubscriptionData(); + if (success) { + renderSubscriptionList(); + } else { + $('#subscription-list-container').html('

    Не удалось загрузить данные. Проверьте соединение с сервером.

    '); + } + }, + unmount: () => { + availableResources = {}; + subscribedKeys = []; + } +}; \ No newline at end of file diff --git a/webui/src/styles/app.css b/webui/src/styles/app.css index 131f884..14dc3c9 100644 --- a/webui/src/styles/app.css +++ b/webui/src/styles/app.css @@ -138,8 +138,9 @@ html.dark-theme, body { color: white; } .logout-btn { - margin-top: auto; /* Прижимает к низу */ + margin-top: 15px; border-top: 1px solid var(--color-border); + padding-top: 15px; } .content-area { @@ -419,6 +420,8 @@ html.dark-theme, body { padding: 30px; border-radius: var(--border-radius); border: 1px solid var(--color-border); + max-width: 520px; + margin: 0 auto; } .config-section-title { diff --git a/webui/src/ui/api_tokens.js b/webui/src/ui/api_tokens.js deleted file mode 100644 index 12ceca8..0000000 --- a/webui/src/ui/api_tokens.js +++ /dev/null @@ -1,205 +0,0 @@ -import $ from 'jquery'; -import { JSONRPC } from '@/json-rpc.js'; - -// --- Глобальное состояние --- -let tokens = []; - -/** - * @private - * Загружает текущий список токенов. - * @returns {Promise} - */ -async function loadTokens() { - try { - // Запрос списка токенов - tokens = await JSONRPC.Auth.listTokens(); - tokens = tokens || []; - return true; - } catch (e) { - console.error("Ошибка при загрузке токенов:", e); - tokens = []; - return false; - } -} - -/** - * @private - * Рендерит список токенов и кнопки управления. - */ -function renderTokenList() { - const $listContainer = $('#api-token-container'); - - // Кнопка "Создать токен" - const createButtonHtml = ` - - - `; - - if (tokens.length === 0) { - $listContainer.html(createButtonHtml + '

    Активных API токенов не найдено.

    '); - attachEventHandlers(); - return; - } - - let listHtml = tokens.map(item => { - // Мы НЕ показываем полный токен, показываем только его начало/конец для идентификации - const displayToken = item.token ? - `${item.token.substring(0, 8)}...${item.token.substring(item.token.length - 4)}` : - 'Неизвестный токен'; - - return ` -
  • -
    ${item.description}
    -
    - Ключ: ${displayToken} - -
    -
  • - `; - }).join(''); - - const html = ` - ${createButtonHtml} -
      ${listHtml}
    - `; - - $listContainer.html(html); - attachEventHandlers(); -} - -/** - * @private - * Отображает модальное окно для создания токена. - */ -function showCreateTokenModal() { - // Используем простое модальное окно jQuery для минимализма - const description = prompt("Введите описание для нового API токена (например, 'Токен для Telegram бота'):"); - - if (description === null) { - return; // Пользователь нажал Отмена - } - - if (description.trim() === "") { - alert("Описание токена не может быть пустым."); - return; - } - - createToken(description.trim()); -} - -/** - * @private - * Логика создания токена и обновления UI. - * @param {string} description - Описание токена. - */ -async function createToken(description) { - const $btn = $('#create-token-btn'); - const $message = $('#token-status-message'); - - $message.hide().removeClass('error-message').addClass('success-message'); - $btn.prop('disabled', true).text('Создание...'); - - try { - const newTokenValue = await JSONRPC.Auth.createAPIToken(description); - - if (!newTokenValue) { - throw new Error("Сервер не вернул токен."); - } - - // Показываем токен пользователю ОДИН раз - prompt("ВАЖНО: Ваш новый токен API. Сохраните его, он больше не будет показан!", newTokenValue); - - // Перезагружаем список - await loadTokens(); - renderTokenList(); - - $message.text(`Токен "${description}" успешно создан.`).show(); - - } catch (e) { - console.error('Ошибка создания токена:', e); - $message.removeClass('success-message').addClass('error-message').text('Ошибка при создании токена.').show(); - } finally { - $btn.prop('disabled', false).text('Создать токен API'); - setTimeout(() => $message.fadeOut(), 8000); - } -} - -/** - * @private - * Логика удаления токена. - * @param {string} token - Токен для удаления. - */ -async function deleteToken(token) { - if (!confirm("Вы уверены, что хотите удалить этот API токен?")) { - return; - } - - const $item = $(`li.token-item[data-full-token="${token}"]`); - $item.addClass('deleting').css('opacity', 0.5); // Визуальная обратная связь - - try { - await JSONRPC.Auth.removeToken(token); - - // Удаляем из локального состояния - tokens = tokens.filter(item => item.token !== token); - - // Перерисовываем список - renderTokenList(); - - const $message = $('#token-status-message'); - $message.text('Токен успешно удален.').addClass('success-message').show(); - - } catch (e) { - console.error('Ошибка удаления токена:', e); - $item.removeClass('deleting').css('opacity', 1); // Восстанавливаем, если ошибка - const $message = $('#token-status-message'); - $message.removeClass('success-message').addClass('error-message').text('Ошибка при удалении токена.').show(); - } -} - - -/** - * @private - * Прикрепление обработчиков событий. - */ -function attachEventHandlers() { - $('#create-token-btn').off('click').on('click', showCreateTokenModal); - - // Делегирование для кнопок удаления (они перерисовываются) - $('#api-token-container').off('click', '.delete-token-btn').on('click', '.delete-token-btn', function() { - const tokenToDelete = $(this).data('token'); - if (tokenToDelete) { - deleteToken(tokenToDelete); - } - }); -} - - -// --- Функции для роутера --- - -export const APITokensPage = { - render: () => { - return ` -

    API Токены

    -
    -

    Загрузка токенов...

    -
    - `; - }, - mount: async () => { - const success = await loadTokens(); - if (success) { - renderTokenList(); - } else { - $('#api-token-container').html('

    Не удалось загрузить токены. Проверьте соединение с сервером.

    '); - } - }, - unmount: () => { - tokens = []; - $('#api-token-container').off(); // Удаляем все обработчики - } -}; \ No newline at end of file diff --git a/webui/src/ui/components.js b/webui/src/ui/components.js deleted file mode 100644 index 365b319..0000000 --- a/webui/src/ui/components.js +++ /dev/null @@ -1,134 +0,0 @@ -import $ from 'jquery'; -import { JSONRPC } from '@/json-rpc.js'; - -// --- Глобальное состояние для страницы --- -let availableComponents = []; -let enabledComponents = []; - -/** - * @private - * Извлекает короткое имя компонента, беря часть строки после последней точки. - * @param {string} fullName - Полное имя компонента (напр., ru.kirillius.pf.sdn.API). - * @returns {string} Короткое имя (напр., API). - */ -function getShortName(fullName) { - // Находим позицию последней точки - const lastDotIndex = fullName.lastIndexOf('.'); - - // Если точка найдена, возвращаем подстроку после неё. - // Если точки нет (или она в конце), возвращаем полное имя. - if (lastDotIndex > -1 && lastDotIndex < fullName.length - 1) { - return fullName.substring(lastDotIndex + 1); - } - return fullName; -} - - -/** Загружает все необходимые данные для страницы (без изменений) */ -async function loadComponentData() { - try { - enabledComponents = await JSONRPC.System.getEnabledComponents(); - availableComponents = await JSONRPC.System.getAvailableComponents(); - availableComponents.sort(); - - } catch (e) { - console.error("Ошибка при загрузке данных компонентов:", e); - availableComponents = []; - enabledComponents = []; - return false; - } - return true; -} - -/** Рендеринг HTML списка компонентов */ -function renderComponentList() { - const $listContainer = $('#component-list-container'); - - if (availableComponents.length === 0) { - $listContainer.html('

    Нет доступных системных компонентов.

    '); - return; - } - - let listHtml = availableComponents.map(fullName => { - // 🔥 Применяем новую функцию для отображения - const shortName = getShortName(fullName); - - // Проверяем, включен ли компонент - const isChecked = enabledComponents.includes(fullName); // Проверка по полному имени! - - return ` -
  • - -
  • - `; - }).join(''); - - const html = ` -
      ${listHtml}
    - - - `; - - $listContainer.html(html); - attachEventHandlers(); -} - -/** Обработчик кнопки "Сохранить" (без изменений, т.к. работаем с полными именами из value) */ -function attachEventHandlers() { - $('#save-components-btn').on('click', async function() { - const $btn = $(this); - const $message = $('#component-status-message'); - $message.hide().removeClass('success-message').addClass('error-message'); - - // Собираем все отмеченные ключи компонентов (они содержат полные имена) - const newEnabledComponents = $('.component-checkbox:checked').map(function() { - return $(this).val(); - }).get(); - - $btn.prop('disabled', true).text('Сохранение...'); - - try { - await JSONRPC.System.setEnabledComponents(newEnabledComponents); - - enabledComponents = newEnabledComponents; - - $message.text('Настройки компонентов успешно сохранены!').addClass('success-message').show(); - - } catch (e) { - console.error('Ошибка сохранения компонентов:', e); - $message.text('Ошибка при сохранении компонентов.').show(); - } finally { - $btn.prop('disabled', false).text('Сохранить'); - setTimeout(() => $message.fadeOut(), 5000); - } - }); -} - -// --- Функции для роутера (без изменений) --- - -export const ComponentsPage = { - render: () => { - return ` -

    Управление Компонентами

    -
    -

    Загрузка доступных компонентов...

    -
    - `; - }, - mount: async () => { - const success = await loadComponentData(); - if (success) { - renderComponentList(); - } else { - $('#component-list-container').html('

    Не удалось загрузить данные. Проверьте соединение с сервером.

    '); - } - }, - unmount: () => { - availableComponents = []; - enabledComponents = []; - } -}; \ No newline at end of file diff --git a/webui/src/ui/ovpn.js b/webui/src/ui/ovpn.js deleted file mode 100644 index b10caaf..0000000 --- a/webui/src/ui/ovpn.js +++ /dev/null @@ -1,167 +0,0 @@ -// src/ui/ovpn.js - -import $ from 'jquery'; -import { JSONRPC } from '@/json-rpc.js'; - -const OVPN_COMPONENT_NAME = 'ru.kirillius.pf.sdn.External.API.Components.OVPN'; - -// --- Глобальное состояние --- -let config = {}; - -/** Загрузка конфигурации OVPN */ -async function loadConfig() { - try { - const fullConfig = await JSONRPC.System.getComponentConfig(OVPN_COMPONENT_NAME); - config = fullConfig || {}; - return true; - } catch(e) { - console.error("Ошибка при загрузке конфига OVPN:", e); - return false; - } -} - -/** Рендеринг формы */ -function renderOVPNForm() { - const $container = $('#ovpn-config-container'); - - // Деструктурируем для удобства - const shellConfig = config.shellConfig || {}; - - const formHtml = ` -
    -

    Конфигурация Shell (ShellConfig)

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -

    Команда перезапуска

    -
    - - - Команда, которая будет выполнена через Shell для перезапуска сервиса OVPN. -
    - -
    - - -
    - -
    - `; - - $container.html(formHtml); - attachEventHandlers(); -} - -/** Сбор данных из формы */ -function collectConfigFromForm() { - // Получаем текущий пароль, если поле ввода пустое (чтобы не отправлять пустую строку) - const newPassword = $('#ovpn-password').val(); - const oldPassword = config.shellConfig ? config.shellConfig.password : ''; - - const newConfig = { - shellConfig: { - useSSH: $('#ovpn-use-ssh').prop('checked'), - host: $('#ovpn-host').val(), - port: parseInt($('#ovpn-port').val()) || 22, - username: $('#ovpn-username').val(), - // Если пароль не введен, используем старый пароль, иначе - новый - password: newPassword ? newPassword : oldPassword, - }, - restartCommand: $('#ovpn-restart-command').val() - }; - return newConfig; -} - -/** Сохранение конфига */ -async function saveConfig() { - const newConfig = collectConfigFromForm(); - const $btn = $('#save-ovpn-btn'); - const $message = $('#ovpn-status-message'); - - $message.hide().removeClass('success-message').addClass('error-message'); - $btn.prop('disabled', true).text('Сохранение...'); - - try { - await JSONRPC.System.setComponentConfig(OVPN_COMPONENT_NAME, newConfig); - config = newConfig; // Обновляем локальное состояние - $message.text('Конфигурация OVPN успешно сохранена.').addClass('success-message').show(); - - } catch(e) { - console.error('Ошибка сохранения конфига:', e); - $message.text('Ошибка при сохранении конфигурации.').show(); - } finally { - $btn.prop('disabled', false).text('Сохранить Конфигурацию'); - setTimeout(() => $message.fadeOut(), 5000); - } -} - -/** Перезапуск сервиса */ -async function restartService() { - const $btn = $('#restart-ovpn-btn'); - const $message = $('#ovpn-status-message'); - - $message.hide().removeClass('success-message').addClass('error-message'); - $btn.prop('disabled', true).text('Перезапуск...'); - - try { - await JSONRPC.OVPN.restartSystemService(); - $message.text('Сервис OVPN успешно перезапущен.').addClass('success-message').show(); - } catch(e) { - console.error('Ошибка перезапуска:', e); - $message.text('Ошибка при перезапуске сервиса.').show(); - } finally { - $btn.prop('disabled', false).text('Перезапустить Сервис'); - setTimeout(() => $message.fadeOut(), 5000); - } -} - - -/** Прикрепление обработчиков */ -function attachEventHandlers() { - $('#save-ovpn-btn').off('click').on('click', saveConfig); - $('#restart-ovpn-btn').off('click').on('click', restartService); -} - -// --- Объект страницы для роутера --- -export const OVPNConfigPage = { - render: () => { - return ` -

    Настройка OVPN

    -
    -

    Загрузка конфигурации...

    -
    - `; - }, - mount: async () => { - const success = await loadConfig(); - if (success) { - renderOVPNForm(); - } else { - $('#ovpn-config-container').html('

    Не удалось загрузить конфигурацию OVPN.

    '); - } - }, - unmount: () => { - config = {}; - $('#ovpn-config-container').off(); - } -}; \ No newline at end of file diff --git a/webui/src/ui/statistics.js b/webui/src/ui/statistics.js deleted file mode 100644 index aadfd6f..0000000 --- a/webui/src/ui/statistics.js +++ /dev/null @@ -1,198 +0,0 @@ -import $ from 'jquery'; -import { JSONRPC } from '@/json-rpc.js'; - -const $content = () => $('#statistics-content'); - -// 🔥 Переменная для хранения ID интервала опроса -let updateInterval = null; -const POLLING_INTERVAL = 5000; // 5 секунд - -// --- Утилитарные функции для рендеринга UI-блоков --- - -/** Создает HTML-карточку для отображения статуса и ресурсов */ -const createStatusCard = (title, status, buttonId, buttonLabel, isUpdating, resourceStatsHtml = '') => { - const statusClass = status === 'Обновляется' || status === 'Обнаружено' ? 'text-yellow-500' : 'text-green-500'; - - return ` -
    -

    ${title}

    -

    - Статус: ${status} -

    - - ${resourceStatsHtml} -
    - `; -}; - -/** Создает HTML-блок для отображения ресурсов сети */ -const createResourceStatsHtml = (resources) => { - return ` -
    -

    Ресурсы:

    -
    - ${createResourceBadge("Домены", resources.domains.length)} - ${createResourceBadge("ASN", resources.ASN.length)} - ${createResourceBadge("Подсети", resources.subnets.length)} -
    -
    - `; -}; - -/** Создает HTML-бэйдж для отдельного ресурса (с уменьшенным шрифтом) */ -const createResourceBadge = (title, count) => { - return ` -
    - ${count} - ${title} -
    - `; -}; - -// --- Основные функции API и DOM --- - -/** Получение количества активных подписок. */ -async function getSubscriptionCount() { - try { - // Используем обновленный метод getSubscribedResources - let subscribedResources = await JSONRPC.SubscriptionManager.getSubscribedResources(); - return (subscribedResources && subscribedResources.length) || 0; - } catch(e) { - console.error("Ошибка при получении subscribedResources:", e); - return 0; - } -} - -/** Запрос данных о ресурсах сети. */ -async function getNetworkResources() { - try { - let res = await JSONRPC.NetworkManager.getOutputResources(); - // Используем ключи, как они приходят с API: domains, ASN, subnets - return { - domains: res.domains || [], - ASN: res.ASN || [], - subnets: res.subnets || [] - }; - } catch(e) { - console.error("Ошибка при получении ресурсов:", e); - return { domains: [], ASN: [], subnets: [] }; - } -} - -/** 2. Запрос и рендеринг данных о ПО и Статусах */ -async function renderSystemAndManagerStatus() { - const dataHtml = []; - - // --- Получение данных --- - const hasUpdates = await JSONRPC.System.hasUpdates(); - const isSubUpdating = await JSONRPC.SubscriptionManager.isUpdating(); - const isNetUpdating = await JSONRPC.NetworkManager.isUpdating(); - const subCount = await getSubscriptionCount(); - const networkResources = await getNetworkResources(); - - // 1. Информация об обновлении ПО - dataHtml.push(createStatusCard( - "Обновление ПО", - hasUpdates ? "Обнаружено" : "Нет обновлений", - "update-software-btn", - "Проверить", - false - )); - - // 2. Статус менеджера подписок (добавляем статистику) - const subStatsHtml = `
    -

    Подписки:

    -
    - ${createResourceBadge("Активно", subCount)} -
    -
    `; - dataHtml.push(createStatusCard( - "Менеджер Подписок", - isSubUpdating ? "Обновляется" : "Активен", - "update-sub-btn", - "Обновить", - isSubUpdating, - subStatsHtml - )); - - // 3. Статус менеджера сетей (ВКЛЮЧАЕМ РЕСУРСЫ) - const netResourcesHtml = createResourceStatsHtml(networkResources); - dataHtml.push(createStatusCard( - "Менеджер Сетей", - isNetUpdating ? "Обновляется" : "Активен", - "update-net-btn", - "Обновить", - isNetUpdating, - netResourcesHtml - )); - - // Обновляем DOM один раз - $content().html(`
    ${dataHtml.join('')}
    `); - - // Прикрепляем обработчики событий после рендеринга - attachEventHandlers(); -} - -// --- Главная функция монтирования --- -export async function renderStatisticsPage() { - // 1. Очищаем старый интервал (на случай, если он был) - stopPolling(); - - // 2. Первичный рендеринг - await renderSystemAndManagerStatus(); - - console.log("start interval") - // 3. Запускаем опрос каждые 5 секунд - updateInterval = setInterval(renderSystemAndManagerStatus, POLLING_INTERVAL); -} - -/** 🔥 Остановка интервала опроса при уходе со страницы */ -export function stopPolling() { - if (updateInterval) { - console.log("clear interval") - clearInterval(updateInterval); - updateInterval = null; - } -} - - -/** Прикрепление обработчиков к кнопкам */ -function attachEventHandlers() { - // Обработчик для менеджера подписок - $('#update-sub-btn').off('click').on('click', async function() { - const $btn = $(this); - $btn.prop('disabled', true).text('Обновление...'); - - try { - await JSONRPC.SubscriptionManager.triggerUpdate(); - alert("Обновление подписок запущено!"); - renderSystemAndManagerStatus(); // Обновляем немедленно - } catch(e) { - alert("Ошибка при запуске обновления подписок!"); - $btn.prop('disabled', false).text('Обновить'); - } - }); - - // Обработчик для менеджера сетей - $('#update-net-btn').off('click').on('click', async function() { - const $btn = $(this); - $btn.prop('disabled', true).text('Обновление...'); - - try { - // Передаем аргумент true в triggerUpdate - await JSONRPC.NetworkManager.triggerUpdate(true); - alert("Обновление сетей запущено!"); - renderSystemAndManagerStatus(); // Обновляем немедленно - } catch(e) { - alert("Ошибка при запуске обновления сетей!"); - $btn.prop('disabled', false).text('Обновить'); - } - }); - - // Кнопка проверки обновлений ПО - $('#update-software-btn').off('click').on('click', function() { - alert("Проверка обновлений ПО запущена..."); - }); -} \ No newline at end of file diff --git a/webui/src/ui/subscriptions.js b/webui/src/ui/subscriptions.js deleted file mode 100644 index 77dbbde..0000000 --- a/webui/src/ui/subscriptions.js +++ /dev/null @@ -1,126 +0,0 @@ -import $ from 'jquery'; -import { JSONRPC } from '@/json-rpc.js'; - -// --- Глобальное состояние для страницы --- -let availableResources = {}; -let subscribedKeys = []; - -/** Загружает все необходимые данные для страницы */ -async function loadSubscriptionData() { - try { - subscribedKeys = await JSONRPC.SubscriptionManager.getSubscribedResources(); - availableResources = await JSONRPC.SubscriptionManager.getAvailableResources(); - - } catch (e) { - console.error("Ошибка при загрузке данных подписок:", e); - availableResources = {}; - subscribedKeys = []; - return false; - } - return true; -} - -/** Рендеринг HTML списка */ -function renderSubscriptionList() { - const $listContainer = $('#subscription-list-container'); - const availableKeys = Object.keys(availableResources).sort(); - - if (availableKeys.length === 0) { - $listContainer.html('

    Нет доступных ресурсов для подписки.

    '); - return; - } - - let listHtml = availableKeys.map(key => { - const details = availableResources[key]; - const isChecked = subscribedKeys.includes(key); - - // 🔥 Обновлено: Используем description и меняем структуру - const description = details.description || 'Нет описания'; - - // Формируем строку с деталями (теперь на новой строке) - const detailText = ` - Доменов: ${details.domains ? details.domains.length : 0}, - ASN: ${details.ASN ? details.ASN.length : 0}, - Подсетей: ${details.subnets ? details.subnets.length : 0} - `; - - return ` -
  • - -
  • - `; - }).join(''); - - const html = ` -
      ${listHtml}
    - - - `; - - $listContainer.html(html); - attachEventHandlers(); -} - -/** Обработчик кнопки "Сохранить" (остается без изменений) */ -function attachEventHandlers() { - $('#save-subscriptions-btn').on('click', async function() { - const $btn = $(this); - const $message = $('#subscription-status-message'); - $message.hide().removeClass('success-message').addClass('error-message'); - - const newSubscribedKeys = $('.subscription-checkbox:checked').map(function() { - return $(this).val(); - }).get(); - - $btn.prop('disabled', true).text('Сохранение...'); - - try { - await JSONRPC.SubscriptionManager.setSubscribedResources(newSubscribedKeys); - - subscribedKeys = newSubscribedKeys; - - $message.text('Настройки подписок успешно сохранены!').addClass('success-message').show(); - - } catch (e) { - console.error('Ошибка сохранения подписок:', e); - $message.text('Ошибка при сохранении подписок.').show(); - } finally { - $btn.prop('disabled', false).text('Сохранить'); - setTimeout(() => $message.fadeOut(), 5000); - } - }); -} - -// ... (SubscriptionsPage, loadSubscriptionData остаются без изменений) ... - -export const SubscriptionsPage = { - render: () => { - return ` -

    Управление Подписками

    -
    -

    Загрузка доступных ресурсов...

    -
    - `; - }, - mount: async () => { - const success = await loadSubscriptionData(); - if (success) { - renderSubscriptionList(); - } else { - $('#subscription-list-container').html('

    Не удалось загрузить данные. Проверьте соединение с сервером.

    '); - } - }, - unmount: () => { - availableResources = {}; - subscribedKeys = []; - } -}; \ No newline at end of file