[ { "id": "orbitaCmds01", "name": "orbita-commands", "active": false, "isArchived": false, "nodes": [ { "parameters": { "httpMethod": "POST", "path": "orbita-cmd", "responseMode": "onReceived", "responseData": "={{ { \"ok\": true } }}", "options": {} }, "id": "wh-orbita-0001", "name": "WH Orbita", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [-360, 0], "webhookId": "orbita-cmd-webhook" }, { "parameters": { "jsCode": "// orbita-commands :: /status /alarms /gps -> sendPhoto (QuickChart) + caption\n// Вход: Telegram update (webhook body). Выход: { chatId, photoUrl, caption, skip }\n// Реальные данные Orbita monitor-api (публичный HTTPS, демо-логин).\n\nconst upd = ($input.first().json.body) || $input.first().json || {};\nconst msg = upd.message || upd.edited_message || upd.channel_post || {};\nconst chatId = msg.chat && msg.chat.id;\nconst textRaw = String(msg.text || '').trim();\n\n// Нет текста / нет chat — тихо выходим (напр. callback/иные апдейты)\nif (!chatId || !textRaw) {\n return [{ json: { skip: true, reason: 'no chat/text' } }];\n}\n\n// команда: первое слово, срезаем @botname\nlet cmd = textRaw.split(/\\s+/)[0].toLowerCase();\ncmd = cmd.replace(/@[a-z0-9_]+$/i, '');\n\nconst API = 'https://monitor.servaki.online';\nconst http = this.helpers.httpRequest;\n\n// ---- helpers ----\nconst pad = n => String(n).padStart(2, '0');\nfunction mskNow() {\n const d = new Date(Date.now() + 3 * 3600 * 1000);\n return pad(d.getUTCDate()) + '.' + pad(d.getUTCMonth() + 1) + ' ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ' МСК';\n}\nfunction qc(config) {\n // QuickChart GET, короткий URL не нужен — Telegram сам скачает\n const c = encodeURIComponent(JSON.stringify(config));\n return 'https://quickchart.io/chart?w=500&h=300&bkg=white&c=' + c;\n}\n\n// ---- справка ----\nif (!['/status', '/alarms', '/gps'].includes(cmd)) {\n const help = 'Орбита · команды\\n' +\n '/status — сводка системы\\n' +\n '/alarms — активные тревоги\\n' +\n '/gps — статус автопарка';\n const banner = 'https://quickchart.io/chart?w=500&h=200&bkg=white&c=' +\n encodeURIComponent(JSON.stringify({ type: 'bar', data: { labels: ['status', 'alarms', 'gps'], datasets: [{ label: 'Orbita', data: [1, 1, 1] }] }, options: { plugins: { title: { display: true, text: 'Servaki Orbita — /status /alarms /gps' } }, legend: { display: false } } }));\n return [{ json: { skip: false, chatId, photoUrl: banner, caption: help, parseMode: 'HTML' } }];\n}\n\n// ---- получаем реальные данные Orbita ----\nlet entities = [], events = [], dataOk = true;\ntry {\n const login = await http({ method: 'POST', url: API + '/api/login', body: { userId: 'demo' }, json: true, timeout: 8000 });\n const token = login && login.token;\n const auth = { Authorization: 'Bearer ' + token };\n entities = await http({ method: 'GET', url: API + '/api/entities', headers: auth, json: true, timeout: 8000 }) || [];\n events = await http({ method: 'GET', url: API + '/api/events', headers: auth, json: true, timeout: 8000 }) || [];\n} catch (e) {\n dataOk = false;\n}\nif (!Array.isArray(entities)) entities = [];\nif (!Array.isArray(events)) events = [];\n\nconst dataNote = dataOk ? '' : '\\n⚠️ данные monitor-api недоступны — показаны 0';\n\nlet photoUrl = '', caption = '';\n\nif (cmd === '/status') {\n const total = entities.length;\n const online = entities.filter(e => e.online).length;\n const offline = total - online;\n const net = entities.filter(e => e.type === 'network');\n const uplink = net.reduce((s, e) => s + ((e.telemetry && e.telemetry.uplink) || 0), 0);\n const clients = net.reduce((s, e) => s + ((e.telemetry && e.telemetry.clients) || 0), 0);\n photoUrl = qc({ type: 'doughnut', data: { labels: ['Онлайн', 'Офлайн'], datasets: [{ data: [online, offline], backgroundColor: ['#22c55e', '#ef4444'] }] }, options: { plugins: { title: { display: true, text: 'Orbita · статус объектов' } } } });\n caption = '📊 Статус системы\\n' +\n 'Объектов: ' + total + ' (онлайн ' + online + ' / офлайн ' + offline + ')\\n' +\n 'Сетевых узлов: ' + net.length + '\\n' +\n 'Клиентов на узлах: ' + clients + '\\n' +\n 'Суммарный аплинк: ' + uplink + ' Мбит/с\\n' +\n '🕒 ' + mskNow() + dataNote;\n} else if (cmd === '/alarms') {\n const crit = events.filter(e => e.severity === 'critical');\n const warn = events.filter(e => e.severity === 'warning');\n const info = events.filter(e => e.severity === 'info');\n const recent = events.slice(0, 3).map(e => {\n const s = e.severity === 'critical' ? '🔴' : (e.severity === 'warning' ? '🟡' : '🔵');\n return s + ' ' + (e.text || e.type || '') + ' · ' + (e.callsign || e.entityId || '');\n }).join('\\n') || 'нет событий';\n photoUrl = qc({ type: 'bar', data: { labels: ['Critical', 'Warning', 'Info'], datasets: [{ label: 'события', data: [crit.length, warn.length, info.length], backgroundColor: ['#ef4444', '#f59e0b', '#3b82f6'] }] }, options: { plugins: { title: { display: true, text: 'Orbita · тревоги' } }, legend: { display: false } } });\n caption = '🚨 Тревоги\\n' +\n '🔴 critical: ' + crit.length + ' 🟡 warning: ' + warn.length + ' 🔵 info: ' + info.length + '\\n' +\n 'Последние:\\n' + recent + '\\n' +\n '🕒 ' + mskNow() + dataNote;\n} else if (cmd === '/gps') {\n const fleet = entities.filter(e => e.type === 'gps' || e.type === 'vehicle' || e.type === 'transport');\n const pool = fleet.length ? fleet : entities; // если нет gps-типа — берём все объекты\n const moving = pool.filter(e => e.online && e.telemetry && (e.telemetry.speed || 0) > 3).length;\n const online = pool.filter(e => e.online).length;\n const parked = online - moving;\n const offline = pool.length - online;\n photoUrl = qc({ type: 'doughnut', data: { labels: ['В движении', 'Стоянка', 'Офлайн'], datasets: [{ data: [moving, parked, offline], backgroundColor: ['#22c55e', '#3b82f6', '#9ca3af'] }] }, options: { plugins: { title: { display: true, text: 'Orbita · автопарк' } } } });\n caption = '🚗 Автопарк / GPS\\n' +\n 'Всего: ' + pool.length + '\\n' +\n '🟢 в движении: ' + moving + '\\n' +\n '🔵 на стоянке: ' + parked + '\\n' +\n '⚪ офлайн: ' + offline + '\\n' +\n '🕒 ' + mskNow() + dataNote;\n}\n\nreturn [{ json: { skip: false, chatId, photoUrl, caption, parseMode: 'HTML' } }];" }, "id": "route-orbita-0003", "name": "Route+Fetch", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-140, -60] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" }, "conditions": [ { "id": "cond-skip", "leftValue": "={{ $json.skip }}", "rightValue": false, "operator": { "type": "boolean", "operation": "false", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "if-send-0004", "name": "IF Send?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [100, -60] }, { "parameters": { "resource": "message", "operation": "sendPhoto", "chatId": "={{ $json.chatId }}", "binaryData": false, "file": "={{ $json.photoUrl }}", "additionalFields": { "caption": "={{ $json.caption }}", "parse_mode": "={{ $json.parseMode }}" } }, "id": "tg-photo-0005", "name": "TG SendPhoto", "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [340, -140], "credentials": { "telegramApi": { "id": "alertsTgCred01", "name": "servaki_online_bot" } }, "continueOnFail": true } ], "connections": { "WH Orbita": { "main": [ [ { "node": "Route+Fetch", "type": "main", "index": 0 } ] ] }, "Route+Fetch": { "main": [[{ "node": "IF Send?", "type": "main", "index": 0 }]] }, "IF Send?": { "main": [[{ "node": "TG SendPhoto", "type": "main", "index": 0 }], []] } }, "settings": { "executionOrder": "v1" }, "pinData": {} } ]