diff --git a/backend/app.js b/backend/app.js index 7b658f2..88948f2 100644 --- a/backend/app.js +++ b/backend/app.js @@ -163,7 +163,7 @@ app.use((req, res, next) => { app.use('/api/tables', tablesRoutes); // ДОЛЖНО БЫТЬ ВЫШЕ! // app.use('/api', identitiesRoutes); app.use('/api/auth', authRoutes); -app.use('/api/users/:userId/tags', userTagsRoutes); +app.use('/api/users', userTagsRoutes); app.use('/api/users', usersRoutes); app.use('/api/chat', chatRoutes); app.use('/api/admin', adminRoutes); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index ccc6c5b..3fa5094 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -5,15 +5,23 @@ const db = require('../db'); // GET /api/messages?userId=123 router.get('/', async (req, res) => { const userId = req.query.userId; - if (!userId) return res.status(400).json({ error: 'userId required' }); try { - const result = await db.getQuery()( - `SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata - FROM messages - WHERE user_id = $1 - ORDER BY created_at ASC`, - [userId] - ); + let result; + if (userId) { + result = await db.getQuery()( + `SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata + FROM messages + WHERE user_id = $1 + ORDER BY created_at ASC`, + [userId] + ); + } else { + result = await db.getQuery()( + `SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata + FROM messages + ORDER BY created_at ASC` + ); + } res.json(result.rows); } catch (e) { res.status(500).json({ error: 'DB error', details: e.message }); diff --git a/backend/server.js b/backend/server.js index 315a666..0aca925 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,6 +3,8 @@ const { app, nonceStore } = require('./app'); const http = require('http'); const { initWSS } = require('./wsHub'); const logger = require('./utils/logger'); +const { getBot } = require('./services/telegramBot'); +const EmailBotService = require('./services/emailBot'); const PORT = process.env.PORT || 8000; @@ -14,9 +16,26 @@ console.log('Используемый порт:', process.env.PORT || 8000); async function initServices() { try { console.log('Инициализация сервисов...'); - // Здесь может быть инициализация ботов, email-сервисов и т.д. - // ... - console.log('Все сервисы успешно инициализированы'); + console.log('[initServices] Запуск Email-бота...'); + console.log('[initServices] Создаю экземпляр EmailBotService...'); + let emailBot; + try { + emailBot = new EmailBotService(); + console.log('[initServices] Экземпляр EmailBotService создан'); + } catch (err) { + console.error('[initServices] Ошибка при создании экземпляра EmailBotService:', err); + throw err; + } + console.log('[initServices] Перед вызовом emailBot.start()'); + try { + await emailBot.start(); + console.log('[initServices] Email-бот успешно запущен'); + } catch (err) { + console.error('[initServices] Ошибка при запуске emailBot:', err); + } + console.log('[initServices] Запуск Telegram-бота...'); + await getBot(); + console.log('[initServices] Telegram-бот успешно запущен'); } catch (error) { console.error('Ошибка при инициализации сервисов:', error); } diff --git a/backend/services/ai-assistant.js b/backend/services/ai-assistant.js index e2828fd..05c6d32 100644 --- a/backend/services/ai-assistant.js +++ b/backend/services/ai-assistant.js @@ -1,3 +1,5 @@ +console.log('[ai-assistant] loaded'); + const { ChatOllama } = require('@langchain/ollama'); const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib'); const { OpenAIEmbeddings } = require('@langchain/openai'); diff --git a/backend/services/aiProviderSettingsService.js b/backend/services/aiProviderSettingsService.js index 080c25e..a1d0df4 100644 --- a/backend/services/aiProviderSettingsService.js +++ b/backend/services/aiProviderSettingsService.js @@ -1,7 +1,6 @@ const db = require('../db'); const OpenAI = require('openai'); const Anthropic = require('@anthropic-ai/sdk'); -const { GoogleGenAI } = require('@google/genai'); const TABLE = 'ai_providers_settings'; @@ -48,6 +47,7 @@ async function getProviderModels(provider, { api_key, base_url } = {}) { return res.data ? res.data.map(m => ({ id: m.id, ...m })) : []; } if (provider === 'google') { + const { GoogleGenAI } = await import('@google/genai'); const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url }); const pager = await ai.models.list(); const models = []; @@ -79,6 +79,7 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) { return { success: true }; } if (provider === 'google') { + const { GoogleGenAI } = await import('@google/genai'); const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url }); const pager = await ai.models.list(); for await (const _ of pager) { diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index fed7dff..1bcac1b 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -1,3 +1,4 @@ +console.log('[EmailBot] emailBot.js loaded'); const db = require('../db'); const nodemailer = require('nodemailer'); const Imap = require('imap'); @@ -9,6 +10,10 @@ const identityService = require('./identity-service'); const aiAssistant = require('./ai-assistant'); class EmailBotService { + constructor() { + console.log('[EmailBot] constructor called'); + } + async getSettingsFromDb() { const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1'); if (!rows.length) throw new Error('Email settings not found in DB'); @@ -46,6 +51,7 @@ class EmailBotService { idleInterval: 300000, forceNoop: true, }, + connTimeout: 30000, // 30 секунд }; } @@ -228,55 +234,63 @@ class EmailBotService { } async start() { - logger.info('[EmailBot] start() called'); - const imapConfig = await this.getImapConfig(); - // Логируем IMAP-конфиг (без пароля) - const safeConfig = { ...imapConfig }; - if (safeConfig.password) safeConfig.password = '***'; - logger.info('[EmailBot] IMAP config:', safeConfig); - let attempt = 0; - const maxAttempts = 3; - this.isChecking = false; - const tryConnect = () => { - attempt++; - logger.info(`[EmailBot] IMAP connect attempt ${attempt}`); - this.imap = new Imap(imapConfig); - this.imap.once('ready', () => { - logger.info('[EmailBot] IMAP connection ready'); - this.imap.openBox('INBOX', false, (err, box) => { - if (err) { - logger.error(`[EmailBot] Error opening INBOX: ${err.message}`); - this.imap.end(); - return; - } - logger.info('[EmailBot] INBOX opened successfully'); + try { + console.log('[EmailBot] start() called'); + logger.info('[EmailBot] start() called'); + const imapConfig = await this.getImapConfig(); + // Логируем IMAP-конфиг (без пароля) + const safeConfig = { ...imapConfig }; + if (safeConfig.password) safeConfig.password = '***'; + logger.info('[EmailBot] IMAP config:', safeConfig); + let attempt = 0; + const maxAttempts = 3; + this.isChecking = false; + const tryConnect = () => { + attempt++; + logger.info(`[EmailBot] IMAP connect attempt ${attempt}`); + this.imap = new Imap(imapConfig); + this.imap.once('ready', () => { + logger.info('[EmailBot] IMAP connection ready'); + this.imap.openBox('INBOX', false, (err, box) => { + if (err) { + logger.error(`[EmailBot] Error opening INBOX: ${err.message}`); + this.imap.end(); + return; + } + logger.info('[EmailBot] INBOX opened successfully'); + }); + // После успешного подключения — обычная логика + this.checkEmails(); + logger.info('[EmailBot] Email bot started and IMAP connection initiated'); + // Периодическая проверка почты + setInterval(async () => { + if (this.isChecking) return; + this.isChecking = true; + try { + await this.checkEmails(); + } catch (e) { + logger.error('[EmailBot] Error in periodic checkEmails:', e); + } + this.isChecking = false; + }, 60000); // 60 секунд }); - // После успешного подключения — обычная логика - this.checkEmails(); - logger.info('[EmailBot] Email bot started and IMAP connection initiated'); - // Периодическая проверка почты - setInterval(async () => { - if (this.isChecking) return; - this.isChecking = true; - try { - await this.checkEmails(); - } catch (e) { - logger.error('[EmailBot] Error in periodic checkEmails:', e); + this.imap.once('error', (err) => { + logger.error(`[EmailBot] IMAP connection error: ${err.message}`); + if (err.message && err.message.toLowerCase().includes('timed out') && attempt < maxAttempts) { + logger.warn(`[EmailBot] IMAP reconnecting in 10 seconds (attempt ${attempt + 1})...`); + setTimeout(tryConnect, 10000); } - this.isChecking = false; - }, 60000); // 60 секунд - }); - this.imap.once('error', (err) => { - logger.error(`[EmailBot] IMAP connection error: ${err.message}`); - if (err.message && err.message.toLowerCase().includes('timed out') && attempt < maxAttempts) { - logger.warn(`[EmailBot] IMAP reconnecting in 10 seconds (attempt ${attempt + 1})...`); - setTimeout(tryConnect, 10000); - } - }); - this.imap.connect(); - }; - tryConnect(); + }); + this.imap.connect(); + }; + tryConnect(); + } catch (err) { + console.error('[EmailBot] Ошибка при старте:', err); + logger.error('[EmailBot] Ошибка при старте:', err); + throw err; + } } } +console.log('[EmailBot] module.exports = EmailBotService'); module.exports = EmailBotService; diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index 94dfb1f..b40682b 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -1,7 +1,10 @@ +console.log('[identity-service] loaded'); + const db = require('../db'); const logger = require('../utils/logger'); const { getLinkedWallet } = require('./wallet-service'); const { checkAdminRole } = require('./admin-role'); +const { broadcastContactsUpdate } = require('../wsHub'); /** * Сервис для работы с идентификаторами пользователей @@ -541,6 +544,7 @@ class IdentityService { await this.saveIdentity(userId, provider, providerId, true); user = { id: userId, role: 'user' }; isNew = true; + broadcastContactsUpdate(); } // Проверяем связь с кошельком const wallet = await getLinkedWallet(user.id); diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 6bad442..ae2c3eb 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -332,8 +332,9 @@ async function getBot() { } }); - // Запускаем бота + // Запуск бота await botInstance.launch(); + logger.info('[TelegramBot] Бот запущен'); } return botInstance; diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue index 89c3682..143dde1 100644 --- a/frontend/src/components/ContactTable.vue +++ b/frontend/src/components/ContactTable.vue @@ -53,8 +53,8 @@ function showDetails(contact) { border-radius: 16px; box-shadow: 0 4px 32px rgba(0,0,0,0.12); padding: 32px 24px 24px 24px; - max-width: 950px; - margin: 40px auto; + width: 100%; + margin-top: 40px; position: relative; overflow-x: auto; } @@ -65,6 +65,9 @@ function showDetails(contact) { margin-bottom: 24px; } .close-btn { + position: absolute; + top: 18px; + right: 18px; background: none; border: none; font-size: 2rem; diff --git a/frontend/src/components/DleManagement.vue b/frontend/src/components/DleManagement.vue index 2e399ab..cb7697f 100644 --- a/frontend/src/components/DleManagement.vue +++ b/frontend/src/components/DleManagement.vue @@ -357,8 +357,8 @@ function uninstallModule(module) { border-radius: 16px; box-shadow: 0 4px 32px rgba(0,0,0,0.12); padding: 32px 24px 24px 24px; - max-width: 950px; - margin: 40px auto; + width: 100%; + margin-top: 40px; position: relative; overflow-x: auto; } diff --git a/frontend/src/components/MessagesTable.vue b/frontend/src/components/MessagesTable.vue new file mode 100644 index 0000000..4476fe9 --- /dev/null +++ b/frontend/src/components/MessagesTable.vue @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index dd722d7..b2ee1d6 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -402,15 +402,15 @@ h3 { /* Медиа-запросы для адаптивности */ @media screen and (min-width: 1200px) { .wallet-sidebar { - width: 30%; - max-width: 350px; + width: 420px; + max-width: 420px; } } @media screen and (min-width: 769px) and (max-width: 1199px) { .wallet-sidebar { - width: 40%; - max-width: 320px; + width: 350px; + max-width: 350px; } } diff --git a/frontend/src/components/tables/TagsTableView.vue b/frontend/src/components/tables/TagsTableView.vue new file mode 100644 index 0000000..6abff4b --- /dev/null +++ b/frontend/src/components/tables/TagsTableView.vue @@ -0,0 +1,285 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/tables/UserTablesList.vue b/frontend/src/components/tables/UserTablesList.vue index 3f1fce1..46880c1 100644 --- a/frontend/src/components/tables/UserTablesList.vue +++ b/frontend/src/components/tables/UserTablesList.vue @@ -1,63 +1,33 @@ \ No newline at end of file diff --git a/frontend/src/composables/useContactsWebSocket.js b/frontend/src/composables/useContactsWebSocket.js new file mode 100644 index 0000000..5f6f4c2 --- /dev/null +++ b/frontend/src/composables/useContactsWebSocket.js @@ -0,0 +1,55 @@ +import { ref, onMounted, onUnmounted } from 'vue'; +import { getContacts } from '../services/contactsService'; +import { getAllMessages } from '../services/messagesService'; + +export function useContactsAndMessagesWebSocket() { + const contacts = ref([]); + const messages = ref([]); + const newContacts = ref([]); + const newMessages = ref([]); + let ws = null; + let lastContactId = null; + let lastMessageId = null; + + async function fetchContacts() { + const all = await getContacts(); + contacts.value = all; + if (lastContactId) { + newContacts.value = all.filter(c => c.id > lastContactId); + } else { + newContacts.value = []; + } + if (all.length) lastContactId = Math.max(...all.map(c => c.id)); + } + + async function fetchMessages() { + const all = await getAllMessages(); + messages.value = all; + if (lastMessageId) { + newMessages.value = all.filter(m => m.id > lastMessageId); + } else { + newMessages.value = []; + } + if (all.length) lastMessageId = Math.max(...all.map(m => m.id)); + } + + onMounted(() => { + fetchContacts(); + fetchMessages(); + ws = new WebSocket('ws://localhost:8000'); + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.type === 'contacts-updated') fetchContacts(); + if (data.type === 'messages-updated') fetchMessages(); + } catch (e) {} + }; + }); + + onUnmounted(() => { if (ws) ws.close(); }); + + function markContactsAsRead() { newContacts.value = []; } + function markMessagesAsRead() { newMessages.value = []; } + + return { contacts, messages, newContacts, newMessages, markContactsAsRead, markMessagesAsRead }; +} \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 3d58510..3ca63d2 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -92,6 +92,11 @@ const routes = [ component: () => import('../views/tables/DeleteTableView.vue'), props: true }, + { + path: '/tables/tags', + name: 'tags-table-view', + component: () => import('../views/tables/TagsTableViewPage.vue') + }, { path: '/contacts/:id', name: 'contact-details', @@ -104,6 +109,16 @@ const routes = [ component: () => import('../views/contacts/ContactDeleteConfirm.vue'), props: true }, + { + path: '/contacts-list', + name: 'contacts-list', + component: () => import('../views/ContactsView.vue') + }, + { + path: '/dle-management', + name: 'dle-management', + component: () => import('../views/DleManagementView.vue') + }, ]; const router = createRouter({ diff --git a/frontend/src/services/contactsService.js b/frontend/src/services/contactsService.js index 4faae53..3a6c73d 100644 --- a/frontend/src/services/contactsService.js +++ b/frontend/src/services/contactsService.js @@ -29,4 +29,13 @@ export default { } return null; } -}; \ No newline at end of file +}; + +export async function getContacts() { + const res = await fetch('/api/users'); + const data = await res.json(); + if (data && data.success) { + return data.contacts; + } + return []; +} \ No newline at end of file diff --git a/frontend/src/services/messagesService.js b/frontend/src/services/messagesService.js index 5117e88..b38c5ea 100644 --- a/frontend/src/services/messagesService.js +++ b/frontend/src/services/messagesService.js @@ -6,4 +6,9 @@ export default { const { data } = await axios.get(`/api/messages?userId=${userId}`); return data; } -}; \ No newline at end of file +}; + +export async function getAllMessages() { + const { data } = await axios.get('/api/messages'); + return data; +} \ No newline at end of file diff --git a/frontend/src/views/ContactsView.vue b/frontend/src/views/ContactsView.vue new file mode 100644 index 0000000..80051ec --- /dev/null +++ b/frontend/src/views/ContactsView.vue @@ -0,0 +1,74 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/CrmView.vue b/frontend/src/views/CrmView.vue index 42de5d8..bfa6b5d 100644 --- a/frontend/src/views/CrmView.vue +++ b/frontend/src/views/CrmView.vue @@ -9,18 +9,16 @@

Управление DLE

-
-

Контакты

-
-

Таблицы