ваше сообщение коммита
This commit is contained in:
@@ -86,6 +86,7 @@ const countriesRoutes = require('./routes/countries'); // Добавляем и
|
||||
const russianClassifiersRoutes = require('./routes/russian-classifiers'); // Добавляем импорт российских классификаторов
|
||||
const ollamaRoutes = require('./routes/ollama'); // Добавляем импорт Ollama маршрутов
|
||||
const aiQueueRoutes = require('./routes/ai-queue'); // Добавляем импорт AI Queue маршрутов
|
||||
const tagsRoutes = require('./routes/tags'); // Добавляем импорт маршрутов тегов
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -210,6 +211,7 @@ app.use('/api/countries', countriesRoutes); // Добавляем маршрут
|
||||
app.use('/api/russian-classifiers', russianClassifiersRoutes); // Добавляем маршрут российских классификаторов
|
||||
app.use('/api/ollama', ollamaRoutes); // Добавляем маршрут Ollama
|
||||
app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI Queue
|
||||
app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов
|
||||
app.use('/api/messages', messagesRoutes);
|
||||
app.use('/api/identities', identitiesRoutes);
|
||||
app.use('/api/rag', ragRoutes); // Подключаем роут
|
||||
|
||||
@@ -19,6 +19,18 @@ const { requireAuth } = require('../middleware/auth');
|
||||
const vectorSearchClient = require('../services/vectorSearchClient');
|
||||
const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHub');
|
||||
|
||||
// Вспомогательная функция для получения ключа шифрования
|
||||
function getEncryptionKey() {
|
||||
const fs = require('fs');
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
|
||||
if (!fs.existsSync(keyPath)) {
|
||||
throw new Error('Encryption key file not found');
|
||||
}
|
||||
|
||||
return fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log('Tables router received:', req.method, req.originalUrl);
|
||||
next();
|
||||
@@ -28,17 +40,12 @@ router.use((req, res, next) => {
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()('SELECT id, created_at, updated_at, is_rag_source_id, decrypt_text(name_encrypted, $1) as name, decrypt_text(description_encrypted, $1) as description FROM user_tables ORDER BY id', [encryptionKey]);
|
||||
@@ -54,17 +61,12 @@ router.post('/', async (req, res, next) => {
|
||||
const { name, description, isRagSourceId } = req.body;
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()(
|
||||
@@ -81,17 +83,12 @@ router.post('/', async (req, res, next) => {
|
||||
router.get('/rag-sources', async (req, res, next) => {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()(
|
||||
@@ -111,17 +108,12 @@ router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
// Выполняем все 4 запроса параллельно для ускорения
|
||||
@@ -164,13 +156,27 @@ function generatePlaceholder(name, existingPlaceholders = []) {
|
||||
return '';
|
||||
}).join('');
|
||||
translit = translit.replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
||||
|
||||
// Если translit пустой, используем fallback
|
||||
if (!translit) {
|
||||
translit = 'column';
|
||||
}
|
||||
|
||||
let base = translit;
|
||||
let candidate = base;
|
||||
let i = 1;
|
||||
|
||||
// Генерируем уникальный плейсхолдер
|
||||
while (existingPlaceholders.includes(candidate)) {
|
||||
candidate = `${base}_${i}`;
|
||||
i++;
|
||||
// Защита от бесконечного цикла
|
||||
if (i > 1000) {
|
||||
candidate = `${base}_${Date.now()}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -190,7 +196,13 @@ router.post('/:id/columns', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -201,8 +213,8 @@ router.post('/:id/columns', async (req, res, next) => {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Получаем уже существующие плейсхолдеры в таблице
|
||||
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
|
||||
// Получаем уже существующие плейсхолдеры во всей базе данных
|
||||
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE placeholder IS NOT NULL', [])).rows;
|
||||
const existingPlaceholders = existing.map(c => c.placeholder).filter(Boolean);
|
||||
const placeholder = generatePlaceholder(name, existingPlaceholders);
|
||||
const result = await db.getQuery()(
|
||||
@@ -228,7 +240,13 @@ router.post('/:id/rows', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -262,7 +280,13 @@ router.get('/:id/rows', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -347,7 +371,13 @@ router.post('/cell', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -365,21 +395,11 @@ router.post('/cell', async (req, res, next) => {
|
||||
[row_id, column_id, value, encryptionKey]
|
||||
);
|
||||
|
||||
// Получаем table_id и проверяем, является ли это таблицей тегов
|
||||
// Получаем table_id
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
|
||||
// Проверяем, является ли это таблицей "Теги клиентов"
|
||||
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tables] Проверяем таблицу:', { tableId, tableName: tableName?.name });
|
||||
if (tableName && tableName.name === 'Теги клиентов') {
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
console.log('🔄 [Tables] Обновление ячейки в таблице тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Получаем всю строку для upsert
|
||||
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
|
||||
if (rowData) {
|
||||
@@ -408,35 +428,26 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
// Получаем table_id
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
||||
|
||||
// Проверяем, является ли это таблицей тегов перед удалением
|
||||
let isTagsTable = false;
|
||||
if (table) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
if (!table) {
|
||||
return res.status(404).json({ error: 'Row not found' });
|
||||
}
|
||||
|
||||
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [table.table_id, encryptionKey])).rows[0];
|
||||
isTagsTable = tableName && tableName.name === 'Теги клиентов';
|
||||
}
|
||||
const tableId = table.table_id;
|
||||
|
||||
// Удаляем строку
|
||||
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
||||
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
// Получаем все строки для rebuild
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -453,14 +464,6 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
if (rebuildRows.length > 0) {
|
||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление, если это была таблица тегов
|
||||
if (isTagsTable) {
|
||||
console.log('🔄 [Tables] Обновление строки в таблице тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||
const { broadcastTableUpdate } = require('../wsHub');
|
||||
@@ -468,6 +471,7 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[DELETE /row/:rowId] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -476,9 +480,29 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
router.delete('/column/:columnId', async (req, res, next) => {
|
||||
try {
|
||||
const columnId = req.params.columnId;
|
||||
|
||||
// Получаем информацию о столбце перед удалением
|
||||
const columnInfo = (await db.getQuery()('SELECT table_id FROM user_columns WHERE id = $1', [columnId])).rows[0];
|
||||
if (!columnInfo) {
|
||||
return res.status(404).json({ error: 'Column not found' });
|
||||
}
|
||||
|
||||
// Удаляем все связанные данные в правильном порядке
|
||||
// 1. Удаляем relations, связанные с этим столбцом
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE column_id = $1', [columnId]);
|
||||
|
||||
// 2. Удаляем все значения ячеек для этого столбца
|
||||
await db.getQuery()('DELETE FROM user_cell_values WHERE column_id = $1', [columnId]);
|
||||
|
||||
// 3. Удаляем сам столбец
|
||||
await db.getQuery()('DELETE FROM user_columns WHERE id = $1', [columnId]);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||
broadcastTableUpdate(columnInfo.table_id);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[DELETE /column/:columnId] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -492,7 +516,13 @@ router.patch('/column/:columnId', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -616,7 +646,13 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -665,37 +701,38 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
||||
|
||||
// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем)
|
||||
router.delete('/:id', requireAuth, async (req, res, next) => {
|
||||
const dbModule = require('../db');
|
||||
try {
|
||||
// Логируем строку подключения и pool.options
|
||||
console.log('[DIAG][DELETE] pool.options:', dbModule.pool.options);
|
||||
console.log('[DIAG][DELETE] process.env.DATABASE_URL:', process.env.DATABASE_URL);
|
||||
console.log('[DIAG][DELETE] process.env.DB_HOST:', process.env.DB_HOST);
|
||||
console.log('[DIAG][DELETE] process.env.DB_NAME:', process.env.DB_NAME);
|
||||
console.log('=== [DIAG] Попытка удаления таблицы ===');
|
||||
console.log('Сессия пользователя:', req.session);
|
||||
if (!req.session.isAdmin) {
|
||||
console.log('[DIAG] Нет прав администратора');
|
||||
return res.status(403).json({ error: 'Удаление доступно только администраторам' });
|
||||
}
|
||||
|
||||
const tableId = Number(req.params.id);
|
||||
console.log('[DIAG] id из запроса:', req.params.id, 'Преобразованный id:', tableId, 'typeof:', typeof tableId);
|
||||
|
||||
// Проверяем наличие таблицы перед удалением
|
||||
const checkBefore = await db.getQuery()('SELECT * FROM user_tables WHERE id = $1', [tableId]);
|
||||
console.log('[DIAG] Таблица перед удалением:', checkBefore.rows);
|
||||
const tableExists = (await db.getQuery()('SELECT id FROM user_tables WHERE id = $1', [tableId])).rows[0];
|
||||
if (!tableExists) {
|
||||
return res.status(404).json({ error: 'Table not found' });
|
||||
}
|
||||
|
||||
// Пытаемся удалить
|
||||
// Удаляем все связанные данные в правильном порядке
|
||||
// 1. Удаляем все relations, где эта таблица является источником или целью
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id IN (SELECT id FROM user_rows WHERE table_id = $1) OR to_table_id = $1', [tableId]);
|
||||
|
||||
// 2. Удаляем все значения ячеек для строк этой таблицы
|
||||
await db.getQuery()('DELETE FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId]);
|
||||
|
||||
// 3. Удаляем все строки таблицы
|
||||
await db.getQuery()('DELETE FROM user_rows WHERE table_id = $1', [tableId]);
|
||||
|
||||
// 4. Удаляем все столбцы таблицы
|
||||
await db.getQuery()('DELETE FROM user_columns WHERE table_id = $1', [tableId]);
|
||||
|
||||
// 5. Удаляем саму таблицу
|
||||
const result = await db.getQuery()('DELETE FROM user_tables WHERE id = $1 RETURNING *', [tableId]);
|
||||
console.log('[DIAG] Результат удаления (rowCount):', result.rowCount, 'rows:', result.rows);
|
||||
|
||||
// Проверяем наличие таблицы после удаления
|
||||
const checkAfter = await db.getQuery()('SELECT * FROM user_tables WHERE id = $1', [tableId]);
|
||||
console.log('[DIAG] Таблица после удаления:', checkAfter.rows);
|
||||
|
||||
res.json({ success: true, deleted: result.rowCount });
|
||||
} catch (err) {
|
||||
console.error('[DIAG] Ошибка при удалении таблицы:', err);
|
||||
console.error('[DELETE /:id] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -718,14 +755,51 @@ router.get('/:tableId/row/:rowId/relations', async (req, res, next) => {
|
||||
router.post('/:tableId/row/:rowId/relations', async (req, res, next) => {
|
||||
try {
|
||||
const { tableId, rowId } = req.params;
|
||||
const { column_id, to_table_id, to_row_id } = req.body;
|
||||
const { column_id, to_table_id, to_row_ids } = req.body;
|
||||
|
||||
// Если передается массив to_row_ids - это массовое обновление
|
||||
if (Array.isArray(to_row_ids)) {
|
||||
// Удаляем старые связи для этого столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
|
||||
// Добавляем новые связи
|
||||
if (to_row_ids.length > 0) {
|
||||
const values = to_row_ids.map((to_row_id, index) =>
|
||||
`($1, $2, $3, $${index + 4})`
|
||||
).join(', ');
|
||||
|
||||
const params = [rowId, column_id, to_table_id, ...to_row_ids];
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ${values} RETURNING *`,
|
||||
params
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate(null, rowId);
|
||||
|
||||
res.json(result.rows);
|
||||
} else {
|
||||
res.json([]);
|
||||
}
|
||||
} else {
|
||||
// Одиночная связь
|
||||
const { to_row_id } = req.body;
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate(null, rowId);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[POST /:tableId/row/:rowId/relations] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -742,55 +816,8 @@ router.delete('/:tableId/row/:rowId/relations/:relationId', async (req, res, nex
|
||||
});
|
||||
|
||||
// --- Массовое обновление связей для multiselect-relation ---
|
||||
router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
|
||||
try {
|
||||
const { tableId, rowId } = req.params;
|
||||
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id
|
||||
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
|
||||
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tables] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
|
||||
|
||||
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
|
||||
console.log('🔄 [Tables] Multirelations: обновление тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Удаляем старые связи для этой строки/столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
// Добавляем новые связи
|
||||
for (const to_row_id of to_row_ids) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении связей
|
||||
broadcastTableRelationsUpdate(tableId, rowId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ПРИМЕЧАНИЕ: Для работы с тегами используйте /api/tags/user/:rowId/multirelations
|
||||
// Этот endpoint удален для избежания дублирования
|
||||
|
||||
// Получить плейсхолдеры для всех столбцов таблицы
|
||||
router.get('/:id/placeholders', async (req, res, next) => {
|
||||
@@ -799,7 +826,13 @@ router.get('/:id/placeholders', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
|
||||
134
backend/routes/tags.js
Normal file
134
backend/routes/tags.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
|
||||
console.log('[tags.js] ROUTER LOADED');
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log('[tags.js] ROUTER REQUEST:', req.method, req.originalUrl);
|
||||
next();
|
||||
});
|
||||
|
||||
// PATCH /api/tags/user/:userId — установить теги пользователю
|
||||
router.patch('/user/:userId', async (req, res) => {
|
||||
const userId = Number(req.params.userId);
|
||||
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
|
||||
if (!Array.isArray(tags)) {
|
||||
return res.status(400).json({ error: 'tags должен быть массивом' });
|
||||
}
|
||||
try {
|
||||
// Удаляем старые связи
|
||||
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
|
||||
// Добавляем новые связи
|
||||
for (const tagId of tags) {
|
||||
await db.getQuery()(
|
||||
'INSERT INTO user_tag_links (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||
[userId, tagId]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
broadcastTagsUpdate(null, userId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tags/user/:userId — получить все теги пользователя
|
||||
router.get('/user/:userId', async (req, res) => {
|
||||
const userId = Number(req.params.userId);
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
res.json({ tags: result.rows.map(r => r.tag_id) });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/tags/user/:userId/tag/:tagId — удалить тег у пользователя
|
||||
router.delete('/user/:userId/tag/:tagId', async (req, res) => {
|
||||
const userId = Number(req.params.userId);
|
||||
const tagId = Number(req.params.tagId);
|
||||
try {
|
||||
await db.getQuery()(
|
||||
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
||||
[userId, tagId]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
broadcastTagsUpdate(null, userId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tags/user/:rowId/multirelations — массовое обновление тегов через multirelations
|
||||
router.post('/user/:rowId/multirelations', async (req, res) => {
|
||||
const rowId = Number(req.params.rowId);
|
||||
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id тегов
|
||||
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
|
||||
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tags] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
|
||||
|
||||
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
|
||||
console.log('🔄 [Tags] Multirelations: обновление тегов для строки:', rowId);
|
||||
|
||||
// Удаляем старые связи для этой строки/столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
|
||||
// Добавляем новые связи
|
||||
for (const to_row_id of to_row_ids) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
broadcastTagsUpdate(null, rowId);
|
||||
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(400).json({ error: 'Этот endpoint предназначен только для работы с тегами' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -561,67 +561,11 @@ router.post('/import', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Работа с тегами пользователя через user_tag_links ---
|
||||
// PATCH /api/users/:id/tags — установить теги пользователю
|
||||
router.patch('/:id/tags', async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
|
||||
if (!Array.isArray(tags)) {
|
||||
return res.status(400).json({ error: 'tags должен быть массивом' });
|
||||
}
|
||||
try {
|
||||
// Удаляем старые связи
|
||||
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
|
||||
// Добавляем новые связи
|
||||
for (const tagId of tags) {
|
||||
await db.getQuery()(
|
||||
'INSERT INTO user_tag_links (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||
[userId, tagId]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/users/:id/tags — получить все теги пользователя
|
||||
router.get('/:id/tags', async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
res.json({ tags: result.rows.map(r => r.tag_id) });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/users/:id/tags/:tagId — удалить тег у пользователя
|
||||
router.delete('/:id/tags/:tagId', async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
const tagId = Number(req.params.tagId);
|
||||
try {
|
||||
await db.getQuery()(
|
||||
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
||||
[userId, tagId]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
// --- Работа с тегами перенесена в /api/tags ---
|
||||
// Используйте следующие endpoints:
|
||||
// PATCH /api/tags/user/:id — установить теги пользователю
|
||||
// GET /api/tags/user/:id — получить теги пользователя
|
||||
// DELETE /api/tags/user/:id/tag/:tagId — удалить тег у пользователя
|
||||
// POST /api/tags/user/:id/multirelations — массовое обновление тегов
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -20,6 +20,10 @@ const wsClients = new Map(); // userId -> Set of WebSocket connections
|
||||
const tagsChangeCache = new Map();
|
||||
const TAGS_CACHE_TTL = 5000; // 5 секунд
|
||||
|
||||
// Дебаунс для broadcastTagsUpdate
|
||||
let tagsUpdateTimeout = null;
|
||||
const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
|
||||
|
||||
function initWSS(server) {
|
||||
wss = new WebSocket.Server({ server, path: '/ws' });
|
||||
|
||||
@@ -234,20 +238,32 @@ function broadcastTableRelationsUpdate(tableId, rowId, targetUserId = null) {
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastTagsUpdate(targetUserId = null) {
|
||||
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов');
|
||||
function broadcastTagsUpdate(targetUserId = null, rowId = null) {
|
||||
// Дебаунс: отменяем предыдущий таймаут
|
||||
if (tagsUpdateTimeout) {
|
||||
clearTimeout(tagsUpdateTimeout);
|
||||
}
|
||||
|
||||
// Устанавливаем новый таймаут
|
||||
tagsUpdateTimeout = setTimeout(() => {
|
||||
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов', rowId ? `для строки ${rowId}` : '');
|
||||
const message = JSON.stringify({
|
||||
type: 'tags-updated',
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
rowId: rowId // Добавляем информацию о конкретной строке
|
||||
});
|
||||
|
||||
let sentCount = 0;
|
||||
// Отправляем всем подключенным клиентам
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
console.log('🔔 [WebSocket] Отправляем tags-updated клиенту');
|
||||
client.send(message);
|
||||
sentCount++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🔔 [WebSocket] Отправлено tags-updated ${sentCount} клиентам`);
|
||||
}, TAGS_UPDATE_DEBOUNCE);
|
||||
}
|
||||
|
||||
function getConnectedUsers() {
|
||||
|
||||
@@ -526,7 +526,7 @@ async function loadMultiRelationOptions() {
|
||||
|
||||
// Дебаунсинг для loadMultiRelationValues
|
||||
let loadMultiRelationValuesTimer = null;
|
||||
const LOAD_DEBOUNCE_DELAY = 100; // 100ms
|
||||
const LOAD_DEBOUNCE_DELAY = 50; // 50ms (уменьшено для ускорения)
|
||||
|
||||
async function loadMultiRelationValues() {
|
||||
// Проверяем, не загружены ли уже данные
|
||||
@@ -625,13 +625,20 @@ async function saveMultiRelation() {
|
||||
to_row_ids: editMultiRelationValues.value
|
||||
};
|
||||
console.log('[saveMultiRelation] POST payload:', payload);
|
||||
const response = await fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/multirelations`, {
|
||||
console.log('[TableCell] Отправляем запрос на обновление relations для строки:', props.rowId);
|
||||
console.log('[TableCell] Данные запроса:', payload);
|
||||
const response = await fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/relations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
console.log('[saveMultiRelation] API response status:', response.status, 'result:', result);
|
||||
console.log('[TableCell] Ответ сервера для строки:', props.rowId, 'статус:', response.status, 'результат:', result);
|
||||
if (response.ok) {
|
||||
console.log('[TableCell] Успешно сохранены теги для строки:', props.rowId);
|
||||
} else {
|
||||
console.error('[TableCell] Ошибка сохранения тегов для строки:', props.rowId, 'статус:', response.status);
|
||||
}
|
||||
editing.value = false;
|
||||
await loadMultiRelationValues();
|
||||
console.log('[saveMultiRelation] emitting update with:', editMultiRelationValues.value);
|
||||
@@ -682,6 +689,9 @@ async function addTag() {
|
||||
]);
|
||||
|
||||
console.log('[addTag] Тег добавлен в выбранные:', editMultiRelationValues.value);
|
||||
|
||||
// Сохраняем изменения, чтобы отправить WebSocket уведомление
|
||||
await saveMultiRelation();
|
||||
} catch (e) {
|
||||
console.error('[addTag] Ошибка при добавлении тега:', e);
|
||||
}
|
||||
@@ -707,6 +717,9 @@ async function deleteTag(tagId) {
|
||||
await loadMultiRelationOptions();
|
||||
|
||||
console.log('[deleteTag] Тег удален:', tagId);
|
||||
|
||||
// Сохраняем изменения, чтобы отправить WebSocket уведомление
|
||||
await saveMultiRelation();
|
||||
} catch (e) {
|
||||
console.error('[deleteTag] Ошибка при удалении тега:', e);
|
||||
}
|
||||
|
||||
@@ -90,13 +90,19 @@
|
||||
:resizable="false"
|
||||
>
|
||||
<template #header>
|
||||
<button class="add-col-btn" @click="addColumn" title="Добавить столбец">
|
||||
<button class="add-col-btn" @click.stop="openAddMenu($event)" title="Добавить">
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11" cy="11" r="10" fill="#f3f4f6" stroke="#b6c6e6"/>
|
||||
<rect x="10" y="5.5" width="2" height="11" rx="1" fill="#4f8cff"/>
|
||||
<rect x="5.5" y="10" width="11" height="2" rx="1" fill="#4f8cff"/>
|
||||
</svg>
|
||||
</button>
|
||||
<teleport to="body">
|
||||
<div v-if="showAddMenu" class="context-menu" :style="addMenuStyle">
|
||||
<button class="menu-item" @click="addColumn">Добавить столбец</button>
|
||||
<button class="menu-item" @click="addRow">Добавить строку</button>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<button class="row-menu" @click.stop="openRowMenu(row, $event)">⋮</button>
|
||||
@@ -118,7 +124,7 @@
|
||||
<!-- <button class="menu-item" @click="addColumn">Добавить столбец</button> -->
|
||||
</div>
|
||||
</teleport>
|
||||
<div v-if="openedColMenuId || openedRowMenuId" class="menu-overlay" @click="closeMenus"></div>
|
||||
<div v-if="openedColMenuId || openedRowMenuId || showAddMenu" class="menu-overlay" @click="closeMenus"></div>
|
||||
<!-- Модалка добавления столбца -->
|
||||
<div v-if="showAddColModal" class="modal-backdrop">
|
||||
<div class="modal add-col-modal">
|
||||
@@ -169,7 +175,9 @@ import axios from 'axios';
|
||||
import { ElSelect, ElOption, ElButton } from 'element-plus';
|
||||
import websocketService from '../../services/websocketService';
|
||||
import cacheService from '../../services/cacheService';
|
||||
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
|
||||
let unsubscribeFromTableUpdate = null;
|
||||
let unsubscribeFromTagsUpdate = null;
|
||||
|
||||
const { isAdmin } = useAuthContext();
|
||||
const rebuilding = ref(false);
|
||||
@@ -269,6 +277,10 @@ const openedRowMenuId = ref(null);
|
||||
const colMenuStyle = ref('');
|
||||
const rowMenuStyle = ref('');
|
||||
|
||||
// Меню добавления
|
||||
const showAddMenu = ref(false);
|
||||
const addMenuStyle = ref('');
|
||||
|
||||
function closeAddColModal() {
|
||||
showAddColModal.value = false;
|
||||
newColName.value = '';
|
||||
@@ -546,12 +558,77 @@ onMounted(() => {
|
||||
cacheService.clearTableCache(props.tableId);
|
||||
fetchTable();
|
||||
});
|
||||
|
||||
// Подписка на WebSocket обновления тегов
|
||||
const { onTagsUpdate } = useTagsWebSocket();
|
||||
console.log('[UserTableView] Подписываемся на обновления тегов для таблицы:', props.tableId);
|
||||
console.log('[UserTableView] onTagsUpdate функция:', typeof onTagsUpdate);
|
||||
unsubscribeFromTagsUpdate = onTagsUpdate(async (data) => {
|
||||
console.log('[UserTableView] 🔔 ПОЛУЧЕНО СОБЫТИЕ TAGS-UPDATED!');
|
||||
console.log('[UserTableView] Получено событие tags-updated, обновляем данные для таблицы:', props.tableId, data);
|
||||
|
||||
// Если есть информация о конкретной строке, обновляем только её
|
||||
if (data && data.rowId) {
|
||||
console.log('[UserTableView] Точечное обновление для строки:', data.rowId);
|
||||
try {
|
||||
// Очищаем кэш relations только для конкретной строки
|
||||
const tagColumns = columns.value.filter(col =>
|
||||
col.type === 'multirelation' &&
|
||||
col.options?.relatedTableId
|
||||
);
|
||||
|
||||
for (const col of tagColumns) {
|
||||
cacheService.clearRelationsData(data.rowId, col.id);
|
||||
}
|
||||
|
||||
console.log('[UserTableView] Кэш relations очищен для строки, обновляем данные строки:', data.rowId);
|
||||
|
||||
// Обновляем только данные конкретной строки
|
||||
await updateRowData(data.rowId);
|
||||
console.log('[UserTableView] Данные строки обновлены:', data.rowId);
|
||||
} catch (error) {
|
||||
console.error('[UserTableView] Ошибка при точечном обновлении:', error);
|
||||
// Fallback: полная перезагрузка при ошибке
|
||||
await fetchTable();
|
||||
}
|
||||
} else {
|
||||
// Если нет информации о строке, используем старую логику
|
||||
console.log('[UserTableView] Общее обновление тегов');
|
||||
try {
|
||||
// Очищаем кэш relations для всех строк этой таблицы
|
||||
const tableRows = rows.value || [];
|
||||
for (const row of tableRows) {
|
||||
// Находим колонки с мульти-связями (теги)
|
||||
const tagColumns = columns.value.filter(col =>
|
||||
col.type === 'multirelation' &&
|
||||
col.options?.relatedTableId
|
||||
);
|
||||
|
||||
for (const col of tagColumns) {
|
||||
cacheService.clearRelationsData(row.id, col.id);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[UserTableView] Кэш relations очищен, перезагружаем данные таблицы:', props.tableId);
|
||||
await fetchTable();
|
||||
console.log('[UserTableView] Данные таблицы перезагружены:', props.tableId);
|
||||
} catch (error) {
|
||||
console.error('[UserTableView] Ошибка при обновлении после tags-updated:', error);
|
||||
// Fallback: полная перезагрузка при ошибке
|
||||
cacheService.clearTableCache(props.tableId);
|
||||
await fetchTable();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unsubscribeFromTableUpdate) {
|
||||
unsubscribeFromTableUpdate();
|
||||
}
|
||||
if (unsubscribeFromTagsUpdate) {
|
||||
unsubscribeFromTagsUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
// Для редактирования ячеек
|
||||
@@ -619,6 +696,14 @@ function openRowMenu(row, event) {
|
||||
function closeMenus() {
|
||||
openedColMenuId.value = null;
|
||||
openedRowMenuId.value = null;
|
||||
showAddMenu.value = false;
|
||||
}
|
||||
|
||||
function openAddMenu(event) {
|
||||
showAddMenu.value = true;
|
||||
openedColMenuId.value = null;
|
||||
openedRowMenuId.value = null;
|
||||
setMenuPosition(event, addMenuStyle);
|
||||
}
|
||||
function setMenuPosition(event, styleRef) {
|
||||
// Позиционируем меню под кнопкой
|
||||
@@ -676,6 +761,54 @@ async function rebuildIndex() {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для точечного обновления данных конкретной строки
|
||||
async function updateRowData(rowId) {
|
||||
const startTime = Date.now();
|
||||
console.log(`[UserTableView] 🔄 Начало обновления данных строки ${rowId}`);
|
||||
|
||||
try {
|
||||
// Находим строку в текущих данных
|
||||
const rowIndex = rows.value.findIndex(row => row.id === rowId);
|
||||
if (rowIndex === -1) {
|
||||
console.log(`[UserTableView] Строка ${rowId} не найдена в текущих данных`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Загружаем relations только для этой строки
|
||||
const tagColumns = columns.value.filter(col =>
|
||||
col.type === 'multirelation' &&
|
||||
col.options?.relatedTableId
|
||||
);
|
||||
|
||||
if (tagColumns.length > 0) {
|
||||
console.log(`[UserTableView] 🔄 Загружаем relations для строки ${rowId} (${tagColumns.length} столбцов)`);
|
||||
|
||||
const relationPromises = tagColumns.map(col =>
|
||||
fetch(`/api/tables/${col.table_id}/row/${rowId}/relations`)
|
||||
.then(res => res.json())
|
||||
.then(relations => {
|
||||
// Сохраняем в кэш
|
||||
cacheService.setRelationsData(rowId, col.id, relations);
|
||||
return { rowId, colId: col.id, relations };
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[UserTableView] Ошибка загрузки relations для row:${rowId} col:${col.id}:`, error);
|
||||
return { rowId, colId: col.id, relations: [] };
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(relationPromises);
|
||||
console.log(`[UserTableView] ✅ Relations для строки ${rowId} обновлены`);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
console.log(`[UserTableView] ✅ Завершено обновление строки ${rowId} за ${endTime - startTime}ms`);
|
||||
} catch (error) {
|
||||
console.error(`[UserTableView] ❌ Ошибка при обновлении строки ${rowId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -10,206 +10,40 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import websocketServiceModule from '@/services/websocketService.js';
|
||||
|
||||
const { websocketService, onTableUpdate } = websocketServiceModule;
|
||||
|
||||
export function useTablesWebSocket() {
|
||||
const ws = ref(null);
|
||||
const isConnected = ref(false);
|
||||
const isConnecting = ref(false); // Добавляем флаг для предотвращения множественных подключений
|
||||
const tableUpdateCallbacks = ref(new Map()); // tableId -> callback
|
||||
const tableRelationsUpdateCallbacks = ref(new Map()); // `${tableId}-${rowId}` -> callback
|
||||
const pingInterval = ref(null); // Интервал для ping сообщений
|
||||
|
||||
function connect() {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
console.log('[TablesWebSocket] Уже подключены, пропускаем');
|
||||
return; // Уже подключены
|
||||
}
|
||||
|
||||
if (isConnecting.value) {
|
||||
console.log('[TablesWebSocket] Уже пытаемся подключиться, пропускаем');
|
||||
return; // Уже пытаемся подключиться
|
||||
}
|
||||
|
||||
isConnecting.value = true;
|
||||
|
||||
// Определяем правильный URL для WebSocket
|
||||
let wsUrl;
|
||||
if (import.meta.env.DEV) {
|
||||
// В режиме разработки используем прокси через Vite
|
||||
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
|
||||
} else {
|
||||
// В продакшене используем тот же хост
|
||||
wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws`;
|
||||
}
|
||||
|
||||
console.log('[TablesWebSocket] Подключение к:', wsUrl);
|
||||
console.log('[TablesWebSocket] Текущий хост:', window.location.host);
|
||||
console.log('[TablesWebSocket] Протокол:', window.location.protocol);
|
||||
|
||||
try {
|
||||
ws.value = new WebSocket(wsUrl);
|
||||
} catch (error) {
|
||||
console.error('[TablesWebSocket] Ошибка создания WebSocket:', error);
|
||||
isConnecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('[TablesWebSocket] Соединение установлено');
|
||||
isConnected.value = true;
|
||||
isConnecting.value = false;
|
||||
|
||||
// Запускаем ping каждые 30 секунд
|
||||
pingInterval.value = setInterval(() => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.value.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
|
||||
} catch (error) {
|
||||
console.error('[TablesWebSocket] Ошибка отправки ping:', error);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('[TablesWebSocket] Получено сообщение:', data);
|
||||
|
||||
// Обрабатываем pong ответ
|
||||
if (data.type === 'pong') {
|
||||
console.log('[TablesWebSocket] Получен pong ответ');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'table-updated') {
|
||||
const callbacks = tableUpdateCallbacks.value.get(data.tableId);
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'table-relations-updated') {
|
||||
const key = `${data.tableId}-${data.rowId}`;
|
||||
const callbacks = tableRelationsUpdateCallbacks.value.get(key);
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[TablesWebSocket] Ошибка обработки сообщения:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onclose = (event) => {
|
||||
console.log('[TablesWebSocket] Соединение закрыто', {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean
|
||||
});
|
||||
isConnected.value = false;
|
||||
isConnecting.value = false;
|
||||
|
||||
// Останавливаем ping интервал
|
||||
if (pingInterval.value) {
|
||||
clearInterval(pingInterval.value);
|
||||
pingInterval.value = null;
|
||||
}
|
||||
|
||||
// Переподключение только если это не было намеренное закрытие
|
||||
if (event.code !== 1000) {
|
||||
setTimeout(() => {
|
||||
if (!isConnected.value && !isConnecting.value) {
|
||||
console.log('[TablesWebSocket] Попытка переподключения...');
|
||||
connect();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('[TablesWebSocket] Ошибка соединения:', error);
|
||||
console.error('[TablesWebSocket] WebSocket readyState:', ws.value?.readyState);
|
||||
isConnected.value = false;
|
||||
isConnecting.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
// Подписка на обновления таблиц
|
||||
function subscribeToTableUpdates(tableId, callback) {
|
||||
if (!tableUpdateCallbacks.value.has(tableId)) {
|
||||
tableUpdateCallbacks.value.set(tableId, []);
|
||||
}
|
||||
tableUpdateCallbacks.value.get(tableId).push(callback);
|
||||
|
||||
// Возвращаем функцию для отписки
|
||||
return () => {
|
||||
const callbacks = tableUpdateCallbacks.value.get(tableId);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
if (callbacks.length === 0) {
|
||||
tableUpdateCallbacks.value.delete(tableId);
|
||||
}
|
||||
}
|
||||
};
|
||||
return onTableUpdate(tableId, callback);
|
||||
}
|
||||
|
||||
// Подписка на обновления связей (relations)
|
||||
function subscribeToTableRelationsUpdates(tableId, rowId, callback) {
|
||||
const key = `${tableId}-${rowId}`;
|
||||
if (!tableRelationsUpdateCallbacks.value.has(key)) {
|
||||
tableRelationsUpdateCallbacks.value.set(key, []);
|
||||
}
|
||||
tableRelationsUpdateCallbacks.value.get(key).push(callback);
|
||||
|
||||
// Возвращаем функцию для отписки
|
||||
return () => {
|
||||
const callbacks = tableRelationsUpdateCallbacks.value.get(key);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
if (callbacks.length === 0) {
|
||||
tableRelationsUpdateCallbacks.value.delete(key);
|
||||
}
|
||||
// Используем глобальный обработчик и фильтруем по tableId/rowId
|
||||
const handler = (data) => {
|
||||
if (data.tableId === tableId && data.rowId === rowId) {
|
||||
callback(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (ws.value) {
|
||||
// Останавливаем ping интервал
|
||||
if (pingInterval.value) {
|
||||
clearInterval(pingInterval.value);
|
||||
pingInterval.value = null;
|
||||
}
|
||||
|
||||
// Корректно закрываем соединение
|
||||
if (ws.value.readyState === WebSocket.OPEN) {
|
||||
ws.value.close(1000, 'Manual disconnect');
|
||||
}
|
||||
ws.value = null;
|
||||
}
|
||||
isConnected.value = false;
|
||||
isConnecting.value = false;
|
||||
websocketService.on('table-relations-updated', handler);
|
||||
// Возвращаем функцию для отписки
|
||||
return () => websocketService.off('table-relations-updated', handler);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connect();
|
||||
// Соединение управляется websocketService, ничего не делаем
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect();
|
||||
// Соединение управляется websocketService, ничего не делаем
|
||||
});
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connect,
|
||||
disconnect,
|
||||
subscribeToTableUpdates,
|
||||
subscribeToTableRelationsUpdates
|
||||
subscribeToTableRelationsUpdates,
|
||||
};
|
||||
}
|
||||
@@ -16,14 +16,16 @@ import websocketServiceModule from '../services/websocketService';
|
||||
const { websocketService } = websocketServiceModule;
|
||||
|
||||
export function useTagsWebSocket() {
|
||||
console.log('🏷️ [useTagsWebSocket] Композабл создан');
|
||||
const tagsUpdateCallbacks = ref([]);
|
||||
let debounceTimer = null;
|
||||
const DEBOUNCE_DELAY = 1000; // 1 секунда
|
||||
const isSubscribed = ref(false);
|
||||
|
||||
function onTagsUpdate(callback) {
|
||||
tagsUpdateCallbacks.value.push(callback);
|
||||
console.log('🏷️ [useTagsWebSocket] Регистрация колбэка');
|
||||
|
||||
// Возвращаем функцию для отписки
|
||||
// Проверяем, не зарегистрирован ли уже этот колбэк
|
||||
if (tagsUpdateCallbacks.value.includes(callback)) {
|
||||
console.log('🏷️ [useTagsWebSocket] Колбэк уже зарегистрирован, пропускаем');
|
||||
return () => {
|
||||
const index = tagsUpdateCallbacks.value.indexOf(callback);
|
||||
if (index > -1) {
|
||||
@@ -32,28 +34,60 @@ export function useTagsWebSocket() {
|
||||
};
|
||||
}
|
||||
|
||||
tagsUpdateCallbacks.value.push(callback);
|
||||
console.log('🏷️ [useTagsWebSocket] Количество колбэков:', tagsUpdateCallbacks.value.length);
|
||||
|
||||
// Возвращаем функцию для отписки
|
||||
return () => {
|
||||
console.log('🏷️ [useTagsWebSocket] Отписка колбэка');
|
||||
const index = tagsUpdateCallbacks.value.indexOf(callback);
|
||||
if (index > -1) {
|
||||
tagsUpdateCallbacks.value.splice(index, 1);
|
||||
console.log('🏷️ [useTagsWebSocket] Колбэк удален, осталось:', tagsUpdateCallbacks.value.length);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleTagsUpdate(data) {
|
||||
console.log('🏷️ [useTagsWebSocket] Получено уведомление об обновлении тегов:', data);
|
||||
console.log('🏷️ [useTagsWebSocket] Количество активных колбэков:', tagsUpdateCallbacks.value.length);
|
||||
|
||||
// Вызываем все зарегистрированные колбэки
|
||||
tagsUpdateCallbacks.value.forEach(callback => {
|
||||
tagsUpdateCallbacks.value.forEach((callback, index) => {
|
||||
try {
|
||||
console.log('🏷️ [useTagsWebSocket] Выполняем колбэк #', index);
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error('🏷️ [useTagsWebSocket] Ошибка в колбэке:', error);
|
||||
console.error('🏷️ [useTagsWebSocket] Ошибка в колбэке #', index, ':', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('🏷️ [useTagsWebSocket] onMounted вызван');
|
||||
|
||||
// Проверяем, не подписаны ли уже
|
||||
if (isSubscribed.value) {
|
||||
console.log('🏷️ [useTagsWebSocket] Уже подписаны, пропускаем');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🏷️ [useTagsWebSocket] Подписываемся на tags-updated');
|
||||
websocketService.on('tags-updated', handleTagsUpdate);
|
||||
isSubscribed.value = true;
|
||||
console.log('🏷️ [useTagsWebSocket] Подписка завершена');
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
console.log('🏷️ [useTagsWebSocket] onUnmounted вызван');
|
||||
if (isSubscribed.value) {
|
||||
websocketService.off('tags-updated', handleTagsUpdate);
|
||||
isSubscribed.value = false;
|
||||
console.log('🏷️ [useTagsWebSocket] Отписка завершена');
|
||||
}
|
||||
// Очищаем все колбэки
|
||||
tagsUpdateCallbacks.value = [];
|
||||
console.log('🏷️ [useTagsWebSocket] Колбэки очищены');
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -57,18 +57,18 @@ export default {
|
||||
},
|
||||
// --- Работа с тегами пользователя ---
|
||||
async addTagsToContact(contactId, tagIds) {
|
||||
// PATCH /users/:id/tags { tags: [...] }
|
||||
const res = await api.patch(`/users/${contactId}/tags`, { tags: tagIds });
|
||||
// PATCH /api/tags/user/:id { tags: [...] }
|
||||
const res = await api.patch(`/tags/user/${contactId}`, { tags: tagIds });
|
||||
return res.data;
|
||||
},
|
||||
async getContactTags(contactId) {
|
||||
// GET /users/:id/tags
|
||||
const res = await api.get(`/users/${contactId}/tags`);
|
||||
// GET /api/tags/user/:id
|
||||
const res = await api.get(`/tags/user/${contactId}`);
|
||||
return res.data.tags || [];
|
||||
},
|
||||
async removeTagFromContact(contactId, tagId) {
|
||||
// DELETE /users/:id/tags/:tagId
|
||||
const res = await api.delete(`/users/${contactId}/tags/${tagId}`);
|
||||
// DELETE /api/tags/user/:id/tag/:tagId
|
||||
const res = await api.delete(`/tags/user/${contactId}/tag/${tagId}`);
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
class WebSocketService {
|
||||
constructor() {
|
||||
console.log('🔌 [WebSocket] Конструктор вызван');
|
||||
this.ws = null;
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
@@ -23,10 +24,12 @@ class WebSocketService {
|
||||
this.reconnectDelay = 1000; // 1 секунда
|
||||
this.listeners = new Map();
|
||||
this.userId = null;
|
||||
console.log('🔌 [WebSocket] Конструктор завершен');
|
||||
}
|
||||
|
||||
// Подключение к WebSocket серверу
|
||||
connect(userId = null) {
|
||||
console.log('🔌 [WebSocket] Попытка подключения, userId:', userId);
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
console.log('🔌 [WebSocket] Уже подключен');
|
||||
return;
|
||||
@@ -37,11 +40,11 @@ class WebSocketService {
|
||||
try {
|
||||
// Определяем WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// В Docker окружении backend работает на порту 8000
|
||||
const backendHost = window.location.hostname + ':8000';
|
||||
const wsUrl = `${protocol}//${backendHost}/ws`;
|
||||
// В Docker окружении используем тот же хост, что и для HTTP
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
console.log('🔌 [WebSocket] Подключение к:', wsUrl);
|
||||
console.log('🔌 [WebSocket] Текущий хост:', window.location.host);
|
||||
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
@@ -61,10 +64,35 @@ class WebSocketService {
|
||||
this.emit('connected');
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log('🔌 [WebSocket] Соединение закрыто:', event.code, event.reason);
|
||||
this.isConnected = false;
|
||||
this.emit('disconnected', event);
|
||||
|
||||
// Попытка переподключения
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
console.log(`🔄 [WebSocket] Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect(this.userId);
|
||||
}, this.reconnectDelay * this.reconnectAttempts);
|
||||
} else {
|
||||
console.error('❌ [WebSocket] Превышено максимальное количество попыток переподключения');
|
||||
this.emit('reconnect-failed');
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('❌ [WebSocket] Ошибка соединения:', error);
|
||||
this.emit('error', error);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('📨 [WebSocket] Получено сообщение:', data);
|
||||
console.log('📨 [WebSocket] Тип сообщения:', data.type);
|
||||
this.handleMessage(data);
|
||||
} catch (error) {
|
||||
console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
|
||||
@@ -139,8 +167,9 @@ class WebSocketService {
|
||||
break;
|
||||
|
||||
case 'tags-updated':
|
||||
console.log('🔔 [websocketService] Получено сообщение tags-updated');
|
||||
this.emit('tags-updated');
|
||||
console.log('🔔 [websocketService] Получено сообщение tags-updated:', data);
|
||||
console.log('🔔 [websocketService] Количество слушателей tags-updated:', this.listeners.get('tags-updated')?.length || 0);
|
||||
this.emit('tags-updated', data);
|
||||
break;
|
||||
|
||||
case 'table-updated':
|
||||
@@ -158,10 +187,12 @@ class WebSocketService {
|
||||
|
||||
// Подписка на события
|
||||
on(event, callback) {
|
||||
console.log('🔌 [WebSocket] Подписка на событие:', event);
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
console.log('🔌 [WebSocket] Количество слушателей для', event, ':', this.listeners.get(event).length);
|
||||
}
|
||||
|
||||
// Отписка от событий
|
||||
@@ -177,14 +208,20 @@ class WebSocketService {
|
||||
|
||||
// Эмиссия событий
|
||||
emit(event, data) {
|
||||
console.log('🔌 [WebSocket] Эмиссия события:', event, 'с данными:', data);
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => {
|
||||
const callbacks = this.listeners.get(event);
|
||||
console.log('🔌 [WebSocket] Количество колбэков для', event, ':', callbacks.length);
|
||||
callbacks.forEach((callback, index) => {
|
||||
try {
|
||||
console.log('🔌 [WebSocket] Выполняем колбэк #', index, 'для события', event);
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`❌ [WebSocket] Ошибка в обработчике события ${event}:`, error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('🔌 [WebSocket] Нет слушателей для события:', event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +249,7 @@ class WebSocketService {
|
||||
|
||||
// Создаем единственный экземпляр
|
||||
const websocketService = new WebSocketService();
|
||||
console.log('🔌 [WebSocket] Сервис создан');
|
||||
|
||||
// Подписчики на обновления таблиц: tableId -> [callback]
|
||||
const tableUpdateSubscribers = {};
|
||||
@@ -231,3 +269,17 @@ export default {
|
||||
websocketService,
|
||||
onTableUpdate,
|
||||
};
|
||||
console.log('🔌 [WebSocket] Экспорт завершен');
|
||||
|
||||
// Автоматически подключаемся при загрузке модуля
|
||||
console.log('🔌 [WebSocket] Автоматическое подключение...');
|
||||
setTimeout(() => {
|
||||
console.log('🔌 [WebSocket] Подключаемся через 1 секунду...');
|
||||
websocketService.connect();
|
||||
}, 1000);
|
||||
|
||||
// Добавляем периодическую проверку состояния соединения
|
||||
setInterval(() => {
|
||||
const status = websocketService.getStatus();
|
||||
console.log('🔌 [WebSocket] Статус соединения:', status);
|
||||
}, 10000); // Проверяем каждые 10 секунд
|
||||
Reference in New Issue
Block a user