site-health-timeline
Fetch a per-site health snapshot over a lookback window, including device stability, reboots, WAN uptime, and optional client count. Replaces multiple API calls.
Instructions
Per-site health snapshot over a lookback window: devices with stability scores, reboots, WAN uptime, optional client count. Replaces 5+ sequential calls (devices + wan + reboots + clients). Caveats[] surfaces partial-data and API limitations.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| hostName | Yes | Site host name (e.g. 'USM') | |
| lookbackDays | No | Window for reboot detection in days (1-90, default 7) | |
| extractFields | No | Comma-separated dotted paths to project from response (e.g. 'id,name,owner.name,columns.*.name'). Use `*` as wildcard for arrays/objects. Wrap field names with dots in backticks. Reduces response tokens dramatically on large entities. |
Implementation Reference
- src/tools/aggregations.ts:175-305 (handler)Main handler function for site-health-timeline. Resolves devices by hostName, fetches WAN uptime from /sites, detects reboots from device startupTime (1 per device max), optionally fetches client count via connector. Returns devices with stability scores, reboot summaries, WAN status, client count, and caveats.
export async function siteHealthTimeline(params: z.infer<typeof siteHealthTimelineSchema>) { const lookbackDays = params.lookbackDays ?? 7; const lookbackHours = lookbackDays * 24; const now = new Date(); const start = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000); const period = { start: start.toISOString(), end: now.toISOString(), lookbackDays, }; const caveats: string[] = []; const hostEntry = await resolveDevicesByHostName(params.hostName); if (!hostEntry) { return { hostName: params.hostName, hostId: null, period, devices: [] as DeviceTimelineEntry[], reboots: [] as RebootSummary[], wan: { uptimePct: null, samples: 0, status: "healthy" as WanStatus }, clients: { connector: "unavailable" as const, count: null }, caveats: [`site '${params.hostName}' not found`], }; } const connectorAvailable = isConnectorAvailable(); const { sites, connectorContext } = await aggregate( { sites: () => unifiClient.get<{ data: Array<{ hostId: string; statistics: { wans?: Record<string, { wanUptime?: number; externalIp?: string }> } }> }>("/sites"), connectorContext: connectorAvailable ? () => resolveConnectorContext(params.hostName) : () => Promise.resolve(null), }, caveats, ); // --- WAN aggregation (current / lifetime state from /sites) --- let wanUptimePct: number | null = null; let wanSamples = 0; if (sites) { const siteData = sites.data.find((s) => s.hostId === hostEntry.hostId); const wans = siteData?.statistics.wans ?? {}; const uptimes = Object.values(wans) .map((w) => w.wanUptime) .filter((u): u is number => typeof u === "number"); wanSamples = uptimes.length; if (uptimes.length > 0) { wanUptimePct = Math.round((uptimes.reduce((a, b) => a + b, 0) / uptimes.length) * 10) / 10; } } caveats.push("wan.uptimePct is sourced from /sites.statistics (current/lifetime state); the lookback window applies only to reboot detection"); // --- Devices + reboot detection (reuses logic from detect-recent-reboots) --- const deviceEntries: DeviceTimelineEntry[] = []; const rebootList: RebootSummary[] = []; for (const d of hostEntry.devices) { const hours = hoursAgo(d.startupTime); const rebootedInWindow = hours !== null && hours < lookbackHours; // The Site Manager API exposes only the last startupTime, so reboots // within the window can only ever be 0 or 1 per device. Surface this // limitation in caveats below rather than fabricating a higher count. const rebootsInWindow = rebootedInWindow ? 1 : 0; deviceEntries.push({ id: d.id, name: d.name, model: d.model, status: d.status, startupTime: d.startupTime, rebootsInWindow, stabilityScore: classifyReboots(rebootsInWindow), }); if (rebootedInWindow && hours !== null) { let severity: RebootSummary["severity"] = "info"; if (hours < 1) severity = "critical"; else if (hours < 24) severity = "warning"; rebootList.push({ deviceId: d.id, deviceName: d.name, severity, hoursAgo: Math.round(hours * 10) / 10, }); } } rebootList.sort((a, b) => a.hoursAgo - b.hoursAgo); caveats.push("rebootsInWindow is at most 1 per device — Site Manager API exposes only the most recent startupTime, not full reboot history"); // --- Optional connector clients --- let clientsBlock: { connector: "available" | "unavailable"; count: number | null }; if (!connectorAvailable) { clientsBlock = { connector: "unavailable", count: null }; caveats.push("UNIFI_API_KEY_OWNER not set — client count omitted"); } else if (!connectorContext) { clientsBlock = { connector: "unavailable", count: null }; caveats.push(`connector context unavailable for '${params.hostName}'`); } else { const ctx = connectorContext; const clientsResp = await connectorClient.get<{ data: unknown[] }>( ctx.hostId, `network/integration/v1/sites/${ctx.localSiteId}/clients`, { limit: 1000 }, ).catch((err: unknown) => { caveats.push(`connector clients fetch failed: ${err instanceof Error ? err.message : String(err)}`); return null; }); if (clientsResp && Array.isArray(clientsResp.data)) { clientsBlock = { connector: "available", count: clientsResp.data.length }; } else { clientsBlock = { connector: "available", count: null }; } } return { hostName: hostEntry.hostName, hostId: hostEntry.hostId, period, devices: deviceEntries, reboots: rebootList, wan: { uptimePct: wanUptimePct, samples: wanSamples, status: classifyWan(wanUptimePct), }, clients: clientsBlock, caveats, }; } - src/tools/aggregations.ts:151-156 (schema)Input schema for site-health-timeline: hostName (string), lookbackDays (1-90, default 7), and extractFields.
export const siteHealthTimelineSchema = z.object({ hostName: z.string().describe("Site host name (e.g. 'USM')"), lookbackDays: z.coerce.number().int().min(1).max(90).optional().default(7) .describe("Window for reboot detection in days (1-90, default 7)"), extractFields: ef, }); - src/index.ts:345-347 (registration)Registration of the 'site-health-timeline' tool via the tool() function, wiring the schema shape and wrapped handler.
tool("site-health-timeline", "Per-site health snapshot over a lookback window: devices with stability scores, reboots, WAN uptime, optional client count. Replaces 5+ sequential calls (devices + wan + reboots + clients). Caveats[] surfaces partial-data and API limitations.", siteHealthTimelineSchema.shape, wrapToolHandler(siteHealthTimeline)); - src/index.ts:52-55 (registration)Import of siteHealthTimelineSchema and siteHealthTimeline from ./tools/aggregations.js at the top of index.ts.
import { summarizeSiteSchema, summarizeSite, siteHealthTimelineSchema, siteHealthTimeline, } from "./tools/aggregations.js"; - src/tools/aggregations.ts:14-31 (helper)Helper functions used by the handler: hoursAgo (calculates hours since a timestamp), classifyReboots (maps reboot count to stability score), classifyWan (maps uptime percentage to status).
function hoursAgo(isoTime: string | null | undefined): number | null { if (!isoTime) return null; const diff = Date.now() - new Date(isoTime).getTime(); return diff / (1000 * 60 * 60); } function classifyReboots(count: number): StabilityScore { if (count < 1) return "stable"; if (count <= 3) return "intermittent"; return "unstable"; } function classifyWan(uptimePct: number | null): WanStatus { if (uptimePct === null) return "healthy"; if (uptimePct < 90) return "critical"; if (uptimePct < 95) return "warning"; return "healthy"; }