appspace/query-offline.js
jmcqueen fc5a76b9b8 Initial commit: Appspace + Webex alerting bot
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>
2026-07-01 17:32:44 -04:00

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();