ваше сообщение коммита

This commit is contained in:
2025-07-09 01:18:58 +03:00
parent c18b674364
commit 81dced1f11
54 changed files with 15732 additions and 214 deletions

View File

@@ -87,33 +87,33 @@ const filteredEmbeddingModels = computed(() => {
return embeddingModels.value.filter(m => m.provider === selectedLLM.value.provider);
});
async function loadUserTables() {
const { data } = await axios.get('/api/tables');
const { data } = await axios.get('/tables');
userTables.value = Array.isArray(data) ? data : [];
}
async function loadRules() {
const { data } = await axios.get('/api/settings/ai-assistant-rules');
const { data } = await axios.get('/settings/ai-assistant-rules');
rulesList.value = data.rules || [];
}
async function loadSettings() {
const { data } = await axios.get('/api/settings/ai-assistant');
const { data } = await axios.get('/settings/ai-assistant');
if (data.success && data.settings) {
settings.value = data.settings;
}
}
async function loadTelegramBots() {
const { data } = await axios.get('/api/settings/telegram-settings/list');
const { data } = await axios.get('/settings/telegram-settings/list');
telegramBots.value = data.items || [];
}
async function loadEmailList() {
const { data } = await axios.get('/api/settings/email-settings/list');
const { data } = await axios.get('/settings/email-settings/list');
emailList.value = data.items || [];
}
async function loadLLMModels() {
const { data } = await axios.get('/api/settings/llm-models');
const { data } = await axios.get('/settings/llm-models');
llmModels.value = data.models || [];
}
async function loadEmbeddingModels() {
const { data } = await axios.get('/api/settings/embedding-models');
const { data } = await axios.get('/settings/embedding-models');
embeddingModels.value = data.models || [];
}
onMounted(() => {
@@ -126,7 +126,7 @@ onMounted(() => {
loadEmbeddingModels();
});
async function saveSettings() {
await axios.put('/api/settings/ai-assistant', settings.value);
await axios.put('/settings/ai-assistant', settings.value);
goBack();
}
function openRuleEditor(ruleId = null) {
@@ -139,7 +139,7 @@ function openRuleEditor(ruleId = null) {
}
async function deleteRule(ruleId) {
if (!confirm('Удалить этот набор правил?')) return;
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
await axios.delete(`/settings/ai-assistant-rules/${ruleId}`);
await loadRules();
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
}

View File

@@ -63,7 +63,7 @@ const editMode = ref(false);
const loadDbSettings = async () => {
try {
const res = await api.get('/api/settings/db-settings');
const res = await api.get('/settings/db-settings');
if (res.data.success) {
const s = res.data.settings;
form.dbHost = s.db_host;
@@ -85,7 +85,7 @@ onMounted(async () => {
const saveDbSettings = async () => {
try {
await api.put('/api/settings/db-settings', {
await api.put('/settings/db-settings', {
db_host: form.dbHost,
db_port: form.dbPort,
db_name: form.dbName,

View File

@@ -74,7 +74,7 @@ const editMode = ref(false);
const loadEmailSettings = async () => {
try {
const res = await api.get('/api/settings/email-settings');
const res = await api.get('/settings/email-settings');
if (res.data.success) {
const s = res.data.settings;
form.smtpHost = s.smtp_host;
@@ -98,7 +98,7 @@ onMounted(async () => {
const saveEmailSettings = async () => {
try {
await api.put('/api/settings/email-settings', {
await api.put('/settings/email-settings', {
smtp_host: form.smtpHost,
smtp_port: form.smtpPort,
smtp_user: form.smtpUser,

View File

@@ -45,7 +45,7 @@ const editMode = ref(false);
const loadTelegramSettings = async () => {
try {
const res = await api.get('/api/settings/telegram-settings');
const res = await api.get('/settings/telegram-settings');
if (res.data.success) {
const s = res.data.settings;
form.botToken = s.bot_token || '';
@@ -64,7 +64,7 @@ onMounted(async () => {
const saveTelegramSettings = async () => {
try {
await api.put('/api/telegram-settings', {
await api.put('/telegram-settings', {
bot_token: form.botToken,
bot_username: form.botUsername
});

View File

@@ -70,7 +70,7 @@ const saveError = ref('');
async function loadSettings() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}`);
const { data } = await axios.get(`/settings/ai-settings/${props.provider}`);
if (data.settings) {
apiKey.value = data.settings.api_key || '';
baseUrl.value = data.settings.base_url || '';
@@ -91,7 +91,7 @@ async function loadSettings() {
async function loadModels() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
models.value = data.models || [];
if (!selectedModel.value && models.value.length) {
const first = models.value[0];
@@ -104,7 +104,7 @@ async function loadModels() {
async function loadEmbeddingModels() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
embeddingModels.value = (data.models || []).filter(m => {
const name = m.id || m.name || m;
return name && name.toLowerCase().includes('embed');
@@ -123,7 +123,7 @@ async function onVerify() {
verifyStatus.value = null;
verifyError.value = '';
try {
const { data } = await axios.post(`/api/settings/ai-settings/${props.provider}/verify`, {
const { data } = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
api_key: apiKey.value,
base_url: baseUrl.value,
});
@@ -144,7 +144,7 @@ async function onSave() {
saveStatus.value = null;
saveError.value = '';
try {
await axios.put(`/api/settings/ai-settings/${props.provider}`, {
await axios.put(`/settings/ai-settings/${props.provider}`, {
api_key: apiKey.value,
base_url: baseUrl.value,
selected_model: selectedModel.value,
@@ -161,7 +161,7 @@ async function onSave() {
}
async function onDelete() {
await axios.delete(`/api/settings/ai-settings/${props.provider}`);
await axios.delete(`/settings/ai-settings/${props.provider}`);
apiKey.value = '';
baseUrl.value = '';
selectedModel.value = '';

View File

@@ -93,7 +93,7 @@ async function addToken() {
return;
}
try {
await api.post('/api/settings/auth-token', {
await api.post('/settings/auth-token', {
...newToken,
minBalance: Number(newToken.minBalance) || 0
});

View File

@@ -446,7 +446,7 @@ const fetchIsicCodes = async (params = {}, optionsRef, loadingRef) => {
console.debug(`[BlockchainSettingsView] Fetching ISIC codes with params: ${queryParams}`);
// Убедитесь, что базовый URL настроен правильно (например, через axios interceptors или .env)
const response = await axios.get(`/api/isic/codes?${queryParams}`);
const response = await axios.get(`/isic/codes?${queryParams}`);
if (response.data && Array.isArray(response.data.codes)) {
optionsRef.value = response.data.codes.map(code => ({
@@ -716,7 +716,7 @@ const fetchAddressByZipcode = async () => {
}
console.log(`[FetchByZipcode] Querying backend proxy for Nominatim with: ${params.toString()}`);
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
if (response.data && response.data.length > 0) {
const bestMatch = response.data[0];
@@ -818,7 +818,7 @@ const verifyAddress = async () => {
console.log(`[VerifyAddress] Querying backend proxy for Nominatim with: ${params.toString()}`);
// Запрос теперь идет на ваш бэкенд-прокси
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
// Ответ от бэкенд-прокси должен иметь ту же структуру, что и прямой ответ от Nominatim
if (response.data && Array.isArray(response.data)) { // Проверяем, что это массив (как отвечает Nominatim)
@@ -901,7 +901,7 @@ const toggleShowDeployerKey = () => {
// Функция загрузки настроек RPC с сервера
const loadRpcSettings = async () => {
try {
const response = await axios.get('/api/settings/rpc');
const response = await axios.get('/settings/rpc');
console.log('Ответ сервера на /api/settings/rpc:', response.data);
if (response.data && response.data.success) {
securitySettings.rpcConfigs = (response.data.data || []).map(rpc => ({
@@ -921,7 +921,7 @@ const loadRpcSettings = async () => {
const saveRpcSettings = async () => {
try {
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
const response = await axios.post('/api/settings/rpc', {
const response = await axios.post('/settings/rpc', {
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
});

View File

@@ -73,7 +73,7 @@ async function addRpc() {
return;
}
try {
await api.post('/api/settings/rpc', { networkId, rpcUrl, chainId });
await api.post('/settings/rpc', { networkId, rpcUrl, chainId });
emit('update'); // сигнал родителю перезагрузить список
resetNetworkEntry();
} catch (e) {

View File

@@ -138,7 +138,7 @@ const loadSettings = async () => {
isLoading.value = true;
try {
// Загрузка RPC конфигураций
const rpcResponse = await api.get('/api/settings/rpc');
const rpcResponse = await api.get('/settings/rpc');
if (rpcResponse.data && rpcResponse.data.success) {
securitySettings.rpcConfigs = (rpcResponse.data.data || []).map(rpc => ({
networkId: rpc.network_id,
@@ -149,7 +149,7 @@ const loadSettings = async () => {
}
// Загрузка токенов для аутентификации
const authResponse = await api.get('/api/settings/auth-tokens');
const authResponse = await api.get('/settings/auth-tokens');
if (authResponse.data && authResponse.data.success) {
securitySettings.authTokens = (authResponse.data.data || []).map(token => ({
name: token.name,
@@ -195,12 +195,12 @@ const saveSecuritySettings = async () => {
console.log('[SecuritySettingsView] Отправка настроек на сервер:', settingsData);
// Отправка RPC конфигураций
const rpcResponse = await api.post('/api/settings/rpc', {
const rpcResponse = await api.post('/settings/rpc', {
rpcConfigs: settingsData.rpcConfigs
});
// Отправка токенов для аутентификации
const authResponse = await api.post('/api/settings/auth-tokens', {
const authResponse = await api.post('/settings/auth-tokens', {
authTokens: settingsData.authTokens
});

View File

@@ -20,6 +20,11 @@
<p>Настройки внешнего вида, локализации и пользовательского опыта.</p>
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
</div>
<div class="main-block">
<h3>WEB SSH</h3>
<p>Автоматическая публикация приложения в интернете через SSH-туннель.</p>
<button class="details-btn" @click="$router.push('/settings/webssh')">Подробнее</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,703 @@
<template>
<div class="web-ssh-settings">
<div class="settings-header">
<h2>WEB SSH Туннель</h2>
<p>Автоматическая публикация локального приложения в интернете через SSH-туннель и NGINX</p>
</div>
<div v-if="!agentAvailable" class="agent-instruction-block">
<h3>Установка локального агента</h3>
<ol>
<li>
<b>Windows:</b><br>
Откройте <b>PowerShell</b> или <b>Командную строку</b> и выполните:
<div class="copy-block" @click="copyToClipboard('wsl')">
<pre><code>wsl</code></pre>
<span class="copy-icon">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
Затем в открывшемся терминале WSL выполните:
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
<pre><code>cd ~/DApp-for-Business
sudo bash webssh-agent/install.sh</code></pre>
<span class="copy-icon">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</li>
<li>
<b>Linux:</b><br>
Откройте терминал и выполните:
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
<pre><code>cd ~/DApp-for-Business
sudo bash webssh-agent/install.sh</code></pre>
<span class="copy-icon">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</li>
</ol>
<button @click="checkAgent" class="check-btn">Проверить</button>
<div v-if="copied" class="copied-indicator">Скопировано!</div>
</div>
<!-- Форма публикации всегда доступна -->
<div>
<!-- Статус подключения -->
<div class="connection-status">
<div class="status-indicator" :class="{ 'active': isConnected, 'inactive': !isConnected }"></div>
<span class="status-text">{{ connectionStatus }}</span>
<button v-if="isConnected" @click="disconnectTunnel" class="disconnect-btn">Отключить</button>
</div>
<!-- Форма настроек -->
<form @submit.prevent="handleSubmit" class="tunnel-form">
<div class="form-section">
<h3>Настройки домена</h3>
<div class="form-group">
<label for="domain">Домен *</label>
<input
id="domain"
v-model="form.domain"
type="text"
placeholder="example.com"
required
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="email">Email для SSL *</label>
<input
id="email"
v-model="form.email"
type="email"
placeholder="admin@example.com"
required
:disabled="isConnected"
/>
</div>
</div>
<div class="form-section">
<h3>Настройки SSH сервера</h3>
<div class="form-group">
<label for="sshHost">SSH Host/IP *</label>
<input
id="sshHost"
v-model="form.sshHost"
type="text"
placeholder="192.168.1.100 или server.example.com"
required
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="sshUser">SSH Пользователь *</label>
<input
id="sshUser"
v-model="form.sshUser"
type="text"
placeholder="root"
required
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="sshKey">SSH Приватный ключ *</label>
<textarea
id="sshKey"
v-model="form.sshKey"
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----"
rows="6"
required
:disabled="isConnected"
></textarea>
</div>
</div>
<div class="form-section advanced-section">
<h3>Дополнительные настройки</h3>
<div class="form-row">
<div class="form-group">
<label for="localPort">Локальный порт</label>
<input
id="localPort"
v-model="form.localPort"
type="number"
min="1"
max="65535"
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="serverPort">Порт сервера</label>
<input
id="serverPort"
v-model="form.serverPort"
type="number"
min="1"
max="65535"
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="sshPort">SSH порт</label>
<input
id="sshPort"
v-model="form.sshPort"
type="number"
min="1"
max="65535"
:disabled="isConnected"
/>
</div>
</div>
</div>
<div class="form-actions">
<button
type="submit"
:disabled="isLoading || isConnected"
class="publish-btn"
>
{{ isLoading ? 'Настройка...' : 'Опубликовать' }}
</button>
<button
type="button"
@click="resetForm"
:disabled="isLoading || isConnected"
class="reset-btn"
>
Сбросить
</button>
</div>
</form>
<!-- Лог операций -->
<div class="operation-log" v-if="logs.length > 0">
<h3>Лог операций</h3>
<div class="log-container">
<div
v-for="(log, index) in logs"
:key="index"
class="log-entry"
:class="log.type"
>
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { useWebSshService } from '../../services/webSshService';
const webSshService = useWebSshService();
const agentAvailable = ref(false);
// Реактивные данные
const isLoading = ref(false);
const isConnected = ref(false);
const connectionStatus = ref('Не подключено');
const logs = ref([]);
// Форма
const form = reactive({
domain: '',
email: '',
sshHost: '',
sshUser: '',
sshKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
});
const copied = ref(false);
let copyTimeout = null;
function copyToClipboard(text) {
navigator.clipboard.writeText(text.replace(/\\n/g, '\n')).then(() => {
copied.value = true;
clearTimeout(copyTimeout);
copyTimeout = setTimeout(() => copied.value = false, 1200);
});
}
function validatePrivateKey(key) {
if (!key) return false;
const trimmed = key.trim();
if (!trimmed.startsWith('-----BEGIN OPENSSH PRIVATE KEY-----')) {
console.error('Ключ не начинается с -----BEGIN OPENSSH PRIVATE KEY-----');
return false;
}
if (!trimmed.endsWith('-----END OPENSSH PRIVATE KEY-----')) {
console.error('Ключ не заканчивается на -----END OPENSSH PRIVATE KEY-----');
return false;
}
if (trimmed.split('\n').length < 3) {
console.error('Ключ слишком короткий или не содержит переносов строк');
return false;
}
return true;
}
// Методы
const handleSubmit = async () => {
if (!validateForm()) return;
// Дополнительная валидация приватного ключа
if (!validatePrivateKey(form.sshKey)) {
addLog('error', 'Проверьте формат приватного ключа!');
return;
}
// Логирование ключа (только для отладки!)
console.log('SSH ключ (начало):', form.sshKey.slice(0, 40));
console.log('SSH ключ (конец):', form.sshKey.slice(-40));
console.log('Длина ключа:', form.sshKey.length);
// Логирование отправляемых данных (без самого ключа)
console.log('Данные для агента:', {
...form,
sshKey: form.sshKey ? `[скрыто, длина: ${form.sshKey.length}]` : 'нет ключа'
});
isLoading.value = true;
addLog('info', 'Запуск публикации...');
try {
// Публикация через агента
const result = await webSshService.createTunnel(form);
if (result.success) {
isConnected.value = true;
connectionStatus.value = `Подключено к ${form.domain}`;
addLog('success', 'SSH туннель успешно создан и настроен');
addLog('info', `Ваше приложение доступно по адресу: https://${form.domain}`);
} else {
addLog('error', result.message || 'Ошибка при создании туннеля');
}
} catch (error) {
addLog('error', `Ошибка: ${error.message}`);
} finally {
isLoading.value = false;
}
};
const disconnectTunnel = async () => {
isLoading.value = true;
addLog('info', 'Отключаю SSH туннель...');
try {
const result = await webSshService.disconnectTunnel();
if (result.success) {
isConnected.value = false;
connectionStatus.value = 'Не подключено';
addLog('success', 'SSH туннель отключен');
} else {
addLog('error', result.message || 'Ошибка при отключении туннеля');
}
} catch (error) {
addLog('error', `Ошибка: ${error.message}`);
} finally {
isLoading.value = false;
}
};
const validateForm = () => {
if (!form.domain || !form.email || !form.sshHost || !form.sshUser || !form.sshKey) {
addLog('error', 'Заполните все обязательные поля');
return false;
}
if (!form.email.includes('@')) {
addLog('error', 'Введите корректный email');
return false;
}
if (!form.sshKey.includes('-----BEGIN') || !form.sshKey.includes('-----END')) {
addLog('error', 'SSH ключ должен быть в формате OpenSSH');
return false;
}
return true;
};
const resetForm = () => {
Object.assign(form, {
domain: '',
email: '',
sshHost: '',
sshUser: '',
sshKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
});
logs.value = [];
};
const addLog = (type, message) => {
logs.value.push({
type,
message,
timestamp: new Date()
});
};
const formatTime = (timestamp) => {
return timestamp.toLocaleTimeString();
};
const checkConnectionStatus = async () => {
try {
const status = await webSshService.getStatus();
isConnected.value = status.connected;
connectionStatus.value = status.connected
? `Подключено к ${status.domain}`
: 'Не подключено';
} catch (error) {
console.error('Ошибка проверки статуса:', error);
}
};
const checkAgent = async () => {
const status = await webSshService.checkAgentStatus();
agentAvailable.value = status.running;
};
// Жизненный цикл
onMounted(() => {
checkAgent();
checkConnectionStatus();
// Проверяем статус каждые 30 секунд
const interval = setInterval(checkConnectionStatus, 30000);
onUnmounted(() => {
clearInterval(interval);
});
});
</script>
<style scoped>
.web-ssh-settings {
max-width: 800px;
margin: 0 auto;
}
.settings-header {
margin-bottom: 2rem;
}
.settings-header h2 {
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.settings-header p {
color: var(--color-text-secondary);
font-size: 0.95rem;
}
.connection-status {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--color-background);
border-radius: 8px;
margin-bottom: 2rem;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
transition: background-color 0.3s;
}
.status-indicator.active {
background-color: var(--color-success);
}
.status-indicator.inactive {
background-color: var(--color-text-secondary);
}
.status-text {
font-weight: 500;
flex: 1;
}
.disconnect-btn {
background: var(--color-danger);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.disconnect-btn:hover {
background: var(--color-danger-dark);
}
.tunnel-form {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem;
margin-bottom: 2rem;
}
.form-section {
margin-bottom: 2rem;
}
.form-section h3 {
color: var(--color-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--color-text);
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 0.95rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-primary);
}
.form-group input:disabled,
.form-group textarea:disabled {
background-color: var(--color-background);
cursor: not-allowed;
}
.form-group textarea {
resize: vertical;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
.advanced-section {
border-top: 1px solid var(--color-border);
padding-top: 2rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.publish-btn {
background: var(--color-primary);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: background-color 0.2s;
}
.publish-btn:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.publish-btn:disabled {
background: var(--color-text-secondary);
cursor: not-allowed;
}
.reset-btn {
background: var(--color-background);
color: var(--color-text);
border: 1px solid var(--color-border);
padding: 0.75rem 2rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
.reset-btn:hover:not(:disabled) {
background: var(--color-border);
}
.reset-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.operation-log {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem;
}
.operation-log h3 {
color: var(--color-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.log-container {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 1rem;
background: var(--color-background);
}
.log-entry {
display: flex;
gap: 1rem;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.log-entry:last-child {
margin-bottom: 0;
}
.log-time {
color: var(--color-text-secondary);
font-family: 'Courier New', monospace;
min-width: 80px;
}
.log-message {
flex: 1;
}
.log-entry.info .log-message {
color: var(--color-text);
}
.log-entry.success .log-message {
color: var(--color-success);
}
.log-entry.error .log-message {
color: var(--color-danger);
}
/* Адаптивный дизайн */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.connection-status {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
.agent-instruction-block {
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 8px;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
text-align: left;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.check-btn {
margin-top: 1rem;
background: var(--color-primary);
color: white;
border: none;
padding: 0.5rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
}
pre {
background: #f5f5f5;
border-radius: 4px;
padding: 0.5em 1em;
margin: 0.5em 0;
font-size: 1em;
user-select: all;
white-space: pre-line;
}
.copy-block {
display: flex;
align-items: center;
cursor: pointer;
margin-bottom: 0.5em;
}
.copy-icon {
margin-left: 0.5em;
font-size: 1.2em;
color: #888;
transition: color 0.2s;
display: flex;
align-items: center;
}
.copy-block:hover .copy-icon svg {
stroke: var(--color-primary);
}
.copied-indicator {
color: var(--color-success, #27ae60);
font-weight: 500;
margin-top: 0.5em;
text-align: right;
}
</style>