#!/usr/bin/env node /** * Bulk reboot store iPads (MRD / MR1 / CD in DeviceFriendlyName). * * Fleet-wide scan of Store Workspace ONE MDM, filter iPads by name, issue * SoftReset (same API as appspace restart-offline). Default is dry-run. * * Usage: * node scripts/rebootStoreIpads.js * node scripts/rebootStoreIpads.js --execute --yes --report reboot-ipads.csv * * Auth: WS1_CLIENT_ID, WS1_CLIENT_SECRET, WS1_TENANT_CODE (Store MDM). * Requires REST API Devices Execute on the OAuth client. * * Note: SoftReset only executes on Supervised (DEP) iOS/iPadOS devices. */ import 'dotenv/config'; import fs from 'node:fs'; import readline from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; import { filterTargetStoreIpads, mdmDeviceId, extractStoreFromFriendlyName, } from './lib/mdmIpadFilter.js'; const DEFAULT_CONCURRENCY = 3; const MAX_FLEET_DEVICES = 25000; function parseArgs(argv) { const out = { execute: false, yes: false, concurrency: DEFAULT_CONCURRENCY, limit: null, report: null, help: false, }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; const next = () => argv[++i]; switch (a) { case '--execute': out.execute = true; break; case '--yes': out.yes = true; break; case '--concurrency': out.concurrency = Math.max(1, Number(next()) || DEFAULT_CONCURRENCY); break; case '--limit': out.limit = Number(next()); break; case '--report': out.report = next(); break; case '-h': case '--help': out.help = true; break; default: throw new Error(`Unknown argument: ${a}`); } } return out; } function printHelp() { console.log(`Usage: node scripts/rebootStoreIpads.js [options] Fleet-wide: scans Store WS1 MDM for iPads whose DeviceFriendlyName contains MRD, MR1, or CD, then issues SoftReset (reboot). Options: --execute Send reboot commands (default: dry-run only) --yes Skip interactive confirm when --execute --concurrency Parallel reboots (default: ${DEFAULT_CONCURRENCY}) --limit Cap number of devices processed --report Write audit CSV -h, --help Show this help iOS/iPadOS devices must be Supervised (DEP) for SoftReset to execute. `); } function toCsvRow(cells) { return cells.map((c) => { const s = String(c ?? ''); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }).join(','); } function summarizeDevice(device) { const friendlyName = device.DeviceFriendlyName || '—'; return { storeNum: extractStoreFromFriendlyName(friendlyName) || '—', friendlyName, serial: device.SerialNumber || '—', mdmId: mdmDeviceId(device), model: device.Model || '—', lastSeen: device.LastSeen || device.LastSystemSampleTime || '—', raw: device, }; } async function confirmExecute(count) { const rl = readline.createInterface({ input, output }); try { const answer = await rl.question( `About to SoftReset ${count} iPad(s). Type YES to continue: `, ); return answer.trim().toUpperCase() === 'YES'; } finally { rl.close(); } } function printPreview(rows) { console.log(`\nMatched ${rows.length} iPad(s):\n`); const preview = rows.slice(0, 30); for (const r of preview) { console.log( ` store=${r.storeNum} id=${r.mdmId} serial=${r.serial} name=${r.friendlyName}`, ); } if (rows.length > preview.length) { console.log(` ... and ${rows.length - preview.length} more`); } } async function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.help) { printHelp(); return; } if (!process.env.WS1_CLIENT_ID || !process.env.WS1_CLIENT_SECRET || !process.env.WS1_TENANT_CODE) { console.error('Missing WS1_CLIENT_ID, WS1_CLIENT_SECRET, or WS1_TENANT_CODE in environment.'); process.exit(1); } const { getMDMDevicesByPlatform, sendMDMRebootCommand } = await import('../integrations/mdm/client.js'); const { runPool } = await import('./lib/webexBulk.js'); console.log('Fetching fleet devices from Store MDM (paginated)...'); const allDevices = await getMDMDevicesByPlatform(null, MAX_FLEET_DEVICES); console.log(`MDM returned ${allDevices.length} device(s).`); let matches = filterTargetStoreIpads(allDevices).map(summarizeDevice); matches = matches.filter((m) => m.mdmId != null); const skippedNoId = filterTargetStoreIpads(allDevices).length - matches.length; if (Number.isFinite(opts.limit) && opts.limit > 0) { matches = matches.slice(0, opts.limit); } if (skippedNoId > 0) { console.warn(`Skipped ${skippedNoId} match(es) with no WS1 Id.Value.`); } printPreview(matches); if (matches.length === 0) { console.log('\nNo matching iPads found. Nothing to do.'); return; } if (!opts.execute) { console.log('\nDry-run only — pass --execute to send SoftReset commands.'); return; } if (!opts.yes) { const ok = await confirmExecute(matches.length); if (!ok) { console.log('Cancelled.'); return; } } console.log(`\nSending SoftReset to ${matches.length} device(s) (concurrency=${opts.concurrency})...`); const results = await runPool(matches, opts.concurrency, async (row) => { const reboot = await sendMDMRebootCommand(row.mdmId); return { ...row, status: reboot.success ? 'sent' : 'failed', httpStatus: reboot.status ?? '', error: reboot.error ?? '', }; }); const sent = results.filter((r) => r.ok && r.value.status === 'sent').length; const failed = results.filter((r) => !r.ok || r.value.status === 'failed').length; console.log(`\nDone: ${sent} sent, ${failed} failed.`); const reportRows = results.map((r, i) => { if (r.ok) return r.value; const base = matches[i]; return { ...base, status: 'error', httpStatus: '', error: r.error?.message || String(r.error), }; }); for (const row of reportRows) { if (row.status !== 'sent') { console.log(` FAIL ${row.friendlyName} (${row.serial}): ${row.error || 'unknown'}`); } } if (opts.report) { const header = ['store', 'friendlyName', 'serial', 'mdmId', 'model', 'lastSeen', 'status', 'httpStatus', 'error']; const lines = [ toCsvRow(header), ...reportRows.map((r) => toCsvRow([ r.storeNum, r.friendlyName, r.serial, r.mdmId, r.model, r.lastSeen, r.status, r.httpStatus ?? '', r.error ?? '', ])), ]; fs.writeFileSync(opts.report, `${lines.join('\n')}\n`); console.log(`\nWrote ${opts.report}`); } console.log('\nNote: WS1 queues SoftReset; iPadOS must be Supervised for reboot to execute.'); } main().catch((err) => { console.error(err.message || err); process.exit(1); });