Node/Express service that:
- Receives Appspace outbound webhooks, enriches with Workspace ONE MDM
data (matched by serial), and posts Adaptive Card alerts to Webex.
- Runs a Webex bot in WebSocket mode with two commands:
* `offline [filter]` - lists currently offline / lost / failed
Appspace devices, enriched with per-device MDM facts + console links.
* `restart-offline [filter]` - sends WS1 SoftReset (reboot) to every
currently-offline device that has a WS1 record. Capped at 50 per
invocation with bounded concurrency to protect the WS1 API.
Notes on hardening already applied:
- In-flight promise coalescing in mdm.js and index.js so burst webhook
traffic can't stampede the WS1 token / device-cache refresh or the
Appspace token refresh.
- Structured logger that serializes Error instances (message, stack,
code, axios response.status/data) instead of stringifying to "{}".
- Webex 7439-char message-limit handling: `offline` builds its body
incrementally against a character budget and reports accurate
"N more not shown" truncation.
- Uses string phrases for `framework.hears(...)` so the framework's
`(^| )phrase($| )` wrapper handles group-space @mentions correctly,
and a shared `extractFilterArg()` helper so filter parsing works
identically in DMs and mentioned messages.
Config, Docker, smoke-test profile, and healthcheck included.
Secrets are managed via `.env` (gitignored); see `.env.example`.
Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
No EOL
4.4 KiB
JavaScript
130 lines
No EOL
4.4 KiB
JavaScript
/**
|
|
* DEPRECATED / LEGACY
|
|
*
|
|
* This standalone script is no longer the recommended way to query offline devices.
|
|
*
|
|
* Use the Webex bot command instead:
|
|
* In your Webex space: type `offline` (or `offline some-filter`)
|
|
*
|
|
* The main server (index.js) now provides a much better implementation:
|
|
* - Uses the Appspace refresh token flow (not static APPSPACE_API_TOKEN)
|
|
* - Shares token management with the webhook path
|
|
* - Has filtering, better table output, and MDM enrichment in other flows
|
|
*
|
|
* This file is kept only for emergency/audit use. It uses older env var conventions
|
|
* (APPSPACE_BASE_URL + APPSPACE_API_TOKEN) and performs a one-shot query then exits.
|
|
*
|
|
* Consider removing this file once the bot command has been validated in production.
|
|
*/
|
|
|
|
require('dotenv').config();
|
|
const axios = require('axios');
|
|
|
|
const WEBEX_BOT_TOKEN = process.env.WEBEX_BOT_TOKEN;
|
|
const WEBEX_ROOM_ID = process.env.WEBEX_ROOM_ID;
|
|
const APPSPACE_API_TOKEN = process.env.APPSPACE_API_TOKEN; // Legacy static token
|
|
const APPSPACE_API_BASE_URL = process.env.APPSPACE_BASE_URL || 'https://api.cloud.appspace.com';
|
|
|
|
if (!WEBEX_BOT_TOKEN || !WEBEX_ROOM_ID || !APPSPACE_API_TOKEN) {
|
|
console.error('❌ Missing required env vars: WEBEX_BOT_TOKEN, WEBEX_ROOM_ID, APPSPACE_API_TOKEN');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.warn('⚠️ Running DEPRECATED query-offline.js script. Prefer the "offline" command via the Webex bot.');
|
|
|
|
async function queryOfflineDevices() {
|
|
try {
|
|
console.log('🔍 Querying Appspace for offline / lost devices...');
|
|
|
|
// Example API call - adjust endpoint based on your exact API docs
|
|
// Common pattern: GET /devices with filters for health status
|
|
const response = await axios.get(`${APPSPACE_API_BASE_URL}/api/v3/devices`, {
|
|
headers: {
|
|
Authorization: `Bearer ${APPSPACE_API_TOKEN}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
params: {
|
|
// Filter examples (test and refine in Postman first)
|
|
healthStatus: 'LostCommunication,Offline,Failed', // or use separate calls if needed
|
|
limit: 200,
|
|
// locationId: 'optional-filter',
|
|
// include: 'location,group'
|
|
}
|
|
});
|
|
|
|
const devices = response.data.items || response.data; // adjust based on actual response shape
|
|
|
|
const offlineDevices = devices.filter(d =>
|
|
['LostCommunication', 'Offline', 'Failed'].includes(d.healthStatus || d.status)
|
|
);
|
|
|
|
if (offlineDevices.length === 0) {
|
|
await sendToWebex('✅ **All devices are currently online or in sync.** No offline devices detected.');
|
|
console.log('✅ No offline devices');
|
|
return;
|
|
}
|
|
|
|
// Build a nice Adaptive Card summary
|
|
const facts = offlineDevices.map(d => ({
|
|
title: d.name || d.deviceName || 'Unknown',
|
|
value: `${d.healthStatus || d.status} • ${d.locationName || 'No location'} • IP: ${d.ipAddress || 'N/A'}`
|
|
}));
|
|
|
|
const adaptiveCard = {
|
|
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
"type": "AdaptiveCard",
|
|
"version": "1.3",
|
|
"body": [
|
|
{
|
|
"type": "Container",
|
|
"style": "attention",
|
|
"bleed": true,
|
|
"items": [
|
|
{ "type": "TextBlock", "text": `📊 Offline Devices Snapshot (${offlineDevices.length})`, "weight": "bolder", "size": "medium" }
|
|
]
|
|
},
|
|
{
|
|
"type": "FactSet",
|
|
"facts": facts
|
|
}
|
|
],
|
|
"actions": [
|
|
{
|
|
"type": "Action.OpenUrl",
|
|
"title": "🔗 Open Devices in Appspace Console",
|
|
"url": "https://app3.cloud.appspace.com/console/#!/devices"
|
|
}
|
|
]
|
|
};
|
|
|
|
await sendToWebex(null, adaptiveCard);
|
|
console.log(`✅ Sent ${offlineDevices.length} offline devices to Webex`);
|
|
|
|
} catch (err) {
|
|
console.error('❌ Error querying offline devices:', err.response?.data || err.message);
|
|
await sendToWebex('⚠️ Failed to query offline devices. Check API token and console.');
|
|
}
|
|
}
|
|
|
|
async function sendToWebex(text, card = null) {
|
|
const payload = {
|
|
roomId: WEBEX_ROOM_ID,
|
|
text: text || 'Appspace Offline Devices Report'
|
|
};
|
|
|
|
if (card) {
|
|
payload.attachments = [{
|
|
contentType: "application/vnd.microsoft.card.adaptive",
|
|
content: card
|
|
}];
|
|
}
|
|
|
|
await axios.post('https://webexapis.com/v1/messages', payload, {
|
|
headers: {
|
|
Authorization: `Bearer ${WEBEX_BOT_TOKEN}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
queryOfflineDevices(); |