Default WAN window to 24h + surface /voicediag in /help

Bumps the shared default WAN look-back from 15m to 24h (1440m) so
both /phonestatus follow-ups and /voicediag pick up a full day of
voice-quality signal by default — better for after-the-fact ticket
triage than a live snapshot. Operators wanting real-time behavior
can set WAN_STANDARD_WINDOW_MINUTES=15 or pass --window 15m to
/voicediag.

Also fills in a long-standing help gap: /voicediag was fully
implemented but never listed in /help. Adds it under "AV & phones"
with usage, examples, and notes covering detail mode, --only,
--window, thresholds, kill switch, HTTP shape, and audit logging.
Updates /help phonestatus to call out the 24h WAN follow-up +
cross-link to /voicediag.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jmcqueen 2026-07-09 09:50:30 -04:00
parent b802383441
commit 44ab6559ac
5 changed files with 69 additions and 23 deletions

View file

@ -146,12 +146,13 @@ WEBEX_TOKENS_PATH=./config/webex-service-tokens.json
# WAN_STANDARD_ENABLED=true
# --- WAN look-back window (minutes) used when the operator doesn't
# pass `--window` on /voicediag. Applies to healthscore + LQM.
# Alarms window is max(60, WAN_STANDARD_WINDOW_MINUTES) since
# sub-hour alarm queries are usually too noisy to be actionable.
# Prisma's finest LQM bucket is 5m; anything <= 5 is snapped up.
# Common values: 15 (default, real-time), 60, 360 (6h), 1440 (24h).
# WAN_STANDARD_WINDOW_MINUTES=15
# pass `--window` on /voicediag (and always used by /phonestatus).
# Applies to healthscore + LQM. Alarms window is
# max(60, WAN_STANDARD_WINDOW_MINUTES) since sub-hour alarm queries
# are usually too noisy to be actionable. Prisma's finest LQM
# bucket is 5m; anything <= 5 is snapped up.
# Common values: 15 (real-time), 60, 360 (6h), 1440 (24h — default).
# WAN_STANDARD_WINDOW_MINUTES=1440
# -----------------------------------------------------------------------------
# /voicediag — Store voice-line standards

View file

@ -18,7 +18,8 @@ const SHORT_HELP = {
// AV / phones
avstatus: 'AV / device status for a store (alias: /wostatus)',
phonestatus: 'DECT + IP phone status for a store',
phonestatus: 'DECT + IP phone status for a store (plus 24h WAN follow-up)',
voicediag: 'Deep voice diagnostic: Webex Calling features + SD-WAN quality with fix cards',
// Jira
jirahistory: 'Recent Jira tickets for a store (optionally filtered by component)',
@ -56,9 +57,39 @@ const LONG_HELP = {
notes: [
'Shows DECT basestations + IP phones with Meraki links.',
'Detailed mode adds firmware, serial, SIP details and errors.',
'When the store is Prisma SD-WAN managed (site name `CG<store>` padded to 5 digits), a follow-up **WAN Diagnostics** message arrives with per-path latency/jitter/loss/MOS, site healthscore, and any alarms — averaged over the last 24 hours (override via `WAN_STANDARD_WINDOW_MINUTES`).',
'For an in-depth voice diagnostic with per-user Webex Calling checks + fix cards, use `/voicediag <store>`.',
'Web dashboard: `/phone-store-dashboard.html`.',
],
},
voicediag: {
title: '/voicediag',
usage: [
'/voicediag <store>',
'/voicediag <store> detail',
'/voicediag <store> --only <check1,check2>',
'/voicediag <store> --window <duration>',
'/voicediag list-checks',
],
examples: [
'/voicediag 782',
'/voicediag 782 detail',
'/voicediag 782 --window 15m',
'/voicediag 782 --window 6h --only wanLatency,wanJitter,wanLoss,wanMos',
'/voicediag 782 --only dnd,callForwarding,voicemail',
'/voicediag list-checks',
],
notes: [
'Runs a full battery of per-user Webex Calling checks (DND, call forwarding, voicemail, call intercept, call waiting, outgoing permission, etc.) plus eight SD-WAN checks (site, healthscore, link state, latency, jitter, loss, MOS, alarms) against the store\'s Prisma tenant.',
'Default view hides OK checks and highlights errors/warnings/skipped. Pass `detail` (or `detailed`) to also see OK checks with expanded per-link tables + thresholds + roll-ups.',
'Fixable issues (e.g. DND on, forwarding to wrong number) post a per-issue confirmation card. A single "apply all" card lets you fix everything at once after reviewing.',
'`--only` restricts the run to specific check ids (comma-separated). Use `/voicediag list-checks` to see every registered id + its scope.',
'`--window` overrides the WAN look-back window (accepts `15m`, `1h`, `6h`, `24h`, `1d`, or a bare minute count). Applies to healthscore + LQM (latency/jitter/loss/MOS) fetches. Alarms are floored at 60m. Default is 24h — set `WAN_STANDARD_WINDOW_MINUTES` for a different global default.',
'Voice-quality thresholds default to ITU-T G.114 / RFC 3550. Override any of them via `WAN_STANDARD_*` env vars (see `.env.example`). Kill-switch: `WAN_STANDARD_ENABLED=false` silences the whole WAN bucket.',
'HTTP equivalent: `?storeNum=<n>[&detailed=true][&only=id1,id2][&window=15m]`. HTTP callers see the markdown snapshot only — cards are chat-only.',
'Audit log: every remediation apply/cancel is logged under `voicediag:audit` with the requester identity.',
],
},
'provision-dect': {
title: '/provision-dect',
usage: ['/provision-dect <store>'],
@ -196,7 +227,7 @@ const LONG_HELP = {
const GROUPS = [
{ title: 'Work orders', keys: ['wohistory', 'wosummary', 'woattachments'] },
{ title: 'AV & phones', keys: ['avstatus', 'phonestatus'] },
{ title: 'AV & phones', keys: ['avstatus', 'phonestatus', 'voicediag'] },
{ title: 'Jira', keys: ['jirahistory', 'jiraticket', 'jirapoll'] },
{ title: 'Provisioning', keys: ['provision-dect', 'provision-vc', 'vcmonitor'] },
{ title: 'Admin & bulk', keys: ['offboarduser', 'webexhost', 'bulkavstatuscsv', 'bulkavswitchcsv', 'devicesbymodel'] },

View file

@ -243,17 +243,25 @@ export async function collectSdwanForStore(storeNum, opts = {}) {
};
}
// Default WAN look-back window (24h in minutes). Chosen because the
// primary users of /phonestatus + /voicediag are triaging trouble
// tickets after the fact, where "was voice quality bad today?" is
// more useful than "how does it look right this second?". Operators
// wanting a live snapshot can pass `--window 15m` on /voicediag or
// set WAN_STANDARD_WINDOW_MINUTES=15 in the environment.
const DEFAULT_WINDOW_MINUTES = 1440;
/**
* Snap an arbitrary window request to a reasonable, sane value.
* Falls back to the env default (WAN_STANDARD_WINDOW_MINUTES) or 15.
* Prisma's own upper bound seems to sit around 30 days for aiops
* queries, but voice-quality signal degrades past a day of averaging
* so we cap at 24h for now.
* Falls back to the env default (WAN_STANDARD_WINDOW_MINUTES) or 1440
* (24h). Prisma's own upper bound seems to sit around 30 days for
* aiops queries, but voice-quality signal degrades past a day of
* averaging so we cap at 24h for now.
*/
function normalizeWindowMinutes(m) {
const raw = Number.isFinite(Number(m)) && Number(m) > 0
? Number(m)
: Number(process.env.WAN_STANDARD_WINDOW_MINUTES) || 15;
: Number(process.env.WAN_STANDARD_WINDOW_MINUTES) || DEFAULT_WINDOW_MINUTES;
// Clamp to [1, 1440] (1 min .. 24 h).
return Math.max(1, Math.min(1440, Math.floor(raw)));
}

View file

@ -12,18 +12,21 @@ objects, and hands them to the renderer + adaptive-card layer in
/voicediag <storeNum> default: hides OK checks, posts fixable cards
/voicediag <storeNum> detail include OK checks + expand every details block
/voicediag <storeNum> --only dnd,callForwarding
/voicediag <storeNum> --window 24h override the WAN look-back window (default 15m)
/voicediag <storeNum> --window 15m narrow the WAN look-back (default 24h)
/voicediag list-checks enumerate every registered check + its scope
```
HTTP path: `GET /voicediag?storeNum=<n>[&detailed=true][&only=dnd,callWaiting][&window=24h]`.
HTTP path: `GET /voicediag?storeNum=<n>[&detailed=true][&only=dnd,callWaiting][&window=15m]`.
`--window` accepts `Nm` / `Nh` / `Nd` shorthand (e.g. `15m`, `1h`, `6h`,
`24h`, `1d`) or a bare integer of minutes. Applies to the WAN
healthscore + LQM (per-path latency/jitter/loss/MOS) fetches. Alarms
are floored at 60m regardless — sub-hour alarm queries are usually
too noisy to be actionable. Global default is
`WAN_STANDARD_WINDOW_MINUTES` (env, defaults to 15).
`WAN_STANDARD_WINDOW_MINUTES` (env, defaults to 1440 / 24h). The
same default is used by the `/phonestatus` WAN follow-up (no CLI
override on that surface — set the env if you want a different value
globally).
HTTP callers get the markdown snapshot only — remediation cards are
chat-only.

View file

@ -768,23 +768,26 @@ test('collectSdwanForStore: reports the effective window on the returned payload
});
setupSaseEnv(fake.baseUrl);
try {
// Default (no opts): 15 min (also the env default)
// Default (no opts): 24h — the shipping default optimised for
// "was the site healthy today" over "is it healthy right now".
// Override via WAN_STANDARD_WINDOW_MINUTES or --window on /voicediag.
let data = await collectSdwanForStore(782);
assert.equal(data.window?.minutes, 1440);
assert.equal(data.window?.alarmMinutes, 1440,
'alarms match the requested window when it is >= 60m');
// Explicit 15m override → both windows go to 15/60 (alarm floor)
data = await collectSdwanForStore(782, { windowMinutes: 15 });
assert.equal(data.window?.minutes, 15);
assert.equal(data.window?.alarmMinutes, 60,
'alarms floor at 60m even when the requested window is smaller');
// Explicit 24h override → both windows go to 1440
data = await collectSdwanForStore(782, { windowMinutes: 1440 });
assert.equal(data.window?.minutes, 1440);
assert.equal(data.window?.alarmMinutes, 1440);
// Out-of-range values are clamped to [1, 1440]
data = await collectSdwanForStore(782, { windowMinutes: 99999 });
assert.equal(data.window?.minutes, 1440, 'capped at 24h');
data = await collectSdwanForStore(782, { windowMinutes: -5 });
assert.equal(data.window?.minutes, 15, 'invalid → falls to env default');
assert.equal(data.window?.minutes, 1440, 'invalid → falls to env default (24h)');
} finally {
await fake.close(); _resetSitesCache(); _resetPrismaAuthCache(); clearEnv();
}