Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент. Доступ агентам: репо публичный, push german-токеном. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
8.9 KiB
JSON
97 lines
8.9 KiB
JSON
[
|
||
{
|
||
"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 = '<b>Орбита · команды</b>\\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 = '<b>📊 Статус системы</b>\\n' +\n 'Объектов: <b>' + total + '</b> (онлайн ' + online + ' / офлайн ' + offline + ')\\n' +\n 'Сетевых узлов: <b>' + net.length + '</b>\\n' +\n 'Клиентов на узлах: <b>' + clients + '</b>\\n' +\n 'Суммарный аплинк: <b>' + uplink + '</b> Мбит/с\\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 = '<b>🚨 Тревоги</b>\\n' +\n '🔴 critical: <b>' + crit.length + '</b> 🟡 warning: <b>' + warn.length + '</b> 🔵 info: <b>' + info.length + '</b>\\n' +\n '<b>Последние:</b>\\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 = '<b>🚗 Автопарк / GPS</b>\\n' +\n 'Всего: <b>' + pool.length + '</b>\\n' +\n '🟢 в движении: <b>' + moving + '</b>\\n' +\n '🔵 на стоянке: <b>' + parked + '</b>\\n' +\n '⚪ офлайн: <b>' + offline + '</b>\\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": {}
|
||
}
|
||
]
|