@xyteai/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +245 -0
  3. package/dist/bin/xyte-cli.d.ts +3 -0
  4. package/dist/bin/xyte-cli.d.ts.map +1 -0
  5. package/dist/bin/xyte-cli.js +18 -0
  6. package/dist/bin/xyte-cli.js.map +1 -0
  7. package/dist/cli/index.d.ts +24 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +1185 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/client/catalog.d.ts +6 -0
  12. package/dist/client/catalog.d.ts.map +1 -0
  13. package/dist/client/catalog.js +32 -0
  14. package/dist/client/catalog.js.map +1 -0
  15. package/dist/client/create-client.d.ts +3 -0
  16. package/dist/client/create-client.d.ts.map +1 -0
  17. package/dist/client/create-client.js +235 -0
  18. package/dist/client/create-client.js.map +1 -0
  19. package/dist/config/connectivity.d.ts +19 -0
  20. package/dist/config/connectivity.d.ts.map +1 -0
  21. package/dist/config/connectivity.js +166 -0
  22. package/dist/config/connectivity.js.map +1 -0
  23. package/dist/config/readiness.d.ts +32 -0
  24. package/dist/config/readiness.d.ts.map +1 -0
  25. package/dist/config/readiness.js +96 -0
  26. package/dist/config/readiness.js.map +1 -0
  27. package/dist/config/retry-policy.d.ts +16 -0
  28. package/dist/config/retry-policy.d.ts.map +1 -0
  29. package/dist/config/retry-policy.js +24 -0
  30. package/dist/config/retry-policy.js.map +1 -0
  31. package/dist/contracts/call-envelope.d.ts +74 -0
  32. package/dist/contracts/call-envelope.d.ts.map +1 -0
  33. package/dist/contracts/call-envelope.js +72 -0
  34. package/dist/contracts/call-envelope.js.map +1 -0
  35. package/dist/contracts/problem.d.ts +11 -0
  36. package/dist/contracts/problem.d.ts.map +1 -0
  37. package/dist/contracts/problem.js +55 -0
  38. package/dist/contracts/problem.js.map +1 -0
  39. package/dist/contracts/versions.d.ts +6 -0
  40. package/dist/contracts/versions.d.ts.map +1 -0
  41. package/dist/contracts/versions.js +9 -0
  42. package/dist/contracts/versions.js.map +1 -0
  43. package/dist/http/errors.d.ts +24 -0
  44. package/dist/http/errors.d.ts.map +1 -0
  45. package/dist/http/errors.js +39 -0
  46. package/dist/http/errors.js.map +1 -0
  47. package/dist/http/transport.d.ts +35 -0
  48. package/dist/http/transport.d.ts.map +1 -0
  49. package/dist/http/transport.js +129 -0
  50. package/dist/http/transport.js.map +1 -0
  51. package/dist/index.d.ts +10 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +23 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/mcp/server.d.ts +12 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +404 -0
  58. package/dist/mcp/server.js.map +1 -0
  59. package/dist/namespaces/device.d.ts +38 -0
  60. package/dist/namespaces/device.d.ts.map +1 -0
  61. package/dist/namespaces/device.js +36 -0
  62. package/dist/namespaces/device.js.map +1 -0
  63. package/dist/namespaces/organization.d.ts +27 -0
  64. package/dist/namespaces/organization.d.ts.map +1 -0
  65. package/dist/namespaces/organization.js +30 -0
  66. package/dist/namespaces/organization.js.map +1 -0
  67. package/dist/namespaces/partner.d.ts +18 -0
  68. package/dist/namespaces/partner.d.ts.map +1 -0
  69. package/dist/namespaces/partner.js +21 -0
  70. package/dist/namespaces/partner.js.map +1 -0
  71. package/dist/observability/logger.d.ts +3 -0
  72. package/dist/observability/logger.d.ts.map +1 -0
  73. package/dist/observability/logger.js +21 -0
  74. package/dist/observability/logger.js.map +1 -0
  75. package/dist/observability/tracing.d.ts +3 -0
  76. package/dist/observability/tracing.d.ts.map +1 -0
  77. package/dist/observability/tracing.js +26 -0
  78. package/dist/observability/tracing.js.map +1 -0
  79. package/dist/secure/key-slots.d.ts +8 -0
  80. package/dist/secure/key-slots.d.ts.map +1 -0
  81. package/dist/secure/key-slots.js +47 -0
  82. package/dist/secure/key-slots.js.map +1 -0
  83. package/dist/secure/keychain.d.ts +20 -0
  84. package/dist/secure/keychain.d.ts.map +1 -0
  85. package/dist/secure/keychain.js +170 -0
  86. package/dist/secure/keychain.js.map +1 -0
  87. package/dist/secure/profile-store.d.ts +66 -0
  88. package/dist/secure/profile-store.d.ts.map +1 -0
  89. package/dist/secure/profile-store.js +309 -0
  90. package/dist/secure/profile-store.js.map +1 -0
  91. package/dist/spec/public-endpoints.json +1175 -0
  92. package/dist/tui/animation.d.ts +12 -0
  93. package/dist/tui/animation.d.ts.map +1 -0
  94. package/dist/tui/animation.js +41 -0
  95. package/dist/tui/animation.js.map +1 -0
  96. package/dist/tui/app.d.ts +27 -0
  97. package/dist/tui/app.d.ts.map +1 -0
  98. package/dist/tui/app.js +711 -0
  99. package/dist/tui/app.js.map +1 -0
  100. package/dist/tui/assets/logo.d.ts +5 -0
  101. package/dist/tui/assets/logo.d.ts.map +1 -0
  102. package/dist/tui/assets/logo.js +24 -0
  103. package/dist/tui/assets/logo.js.map +1 -0
  104. package/dist/tui/data-loaders.d.ts +33 -0
  105. package/dist/tui/data-loaders.d.ts.map +1 -0
  106. package/dist/tui/data-loaders.js +250 -0
  107. package/dist/tui/data-loaders.js.map +1 -0
  108. package/dist/tui/dispatch.d.ts +14 -0
  109. package/dist/tui/dispatch.d.ts.map +1 -0
  110. package/dist/tui/dispatch.js +44 -0
  111. package/dist/tui/dispatch.js.map +1 -0
  112. package/dist/tui/headless-renderer.d.ts +20 -0
  113. package/dist/tui/headless-renderer.d.ts.map +1 -0
  114. package/dist/tui/headless-renderer.js +598 -0
  115. package/dist/tui/headless-renderer.js.map +1 -0
  116. package/dist/tui/input-controller.d.ts +29 -0
  117. package/dist/tui/input-controller.d.ts.map +1 -0
  118. package/dist/tui/input-controller.js +76 -0
  119. package/dist/tui/input-controller.js.map +1 -0
  120. package/dist/tui/key-wizard.d.ts +29 -0
  121. package/dist/tui/key-wizard.d.ts.map +1 -0
  122. package/dist/tui/key-wizard.js +177 -0
  123. package/dist/tui/key-wizard.js.map +1 -0
  124. package/dist/tui/keymap.d.ts +9 -0
  125. package/dist/tui/keymap.d.ts.map +1 -0
  126. package/dist/tui/keymap.js +29 -0
  127. package/dist/tui/keymap.js.map +1 -0
  128. package/dist/tui/layout.d.ts +16 -0
  129. package/dist/tui/layout.d.ts.map +1 -0
  130. package/dist/tui/layout.js +99 -0
  131. package/dist/tui/layout.js.map +1 -0
  132. package/dist/tui/logger.d.ts +12 -0
  133. package/dist/tui/logger.d.ts.map +1 -0
  134. package/dist/tui/logger.js +83 -0
  135. package/dist/tui/logger.js.map +1 -0
  136. package/dist/tui/navigation.d.ts +26 -0
  137. package/dist/tui/navigation.d.ts.map +1 -0
  138. package/dist/tui/navigation.js +136 -0
  139. package/dist/tui/navigation.js.map +1 -0
  140. package/dist/tui/panes.d.ts +7 -0
  141. package/dist/tui/panes.d.ts.map +1 -0
  142. package/dist/tui/panes.js +34 -0
  143. package/dist/tui/panes.js.map +1 -0
  144. package/dist/tui/runtime.d.ts +34 -0
  145. package/dist/tui/runtime.d.ts.map +1 -0
  146. package/dist/tui/runtime.js +100 -0
  147. package/dist/tui/runtime.js.map +1 -0
  148. package/dist/tui/scene.d.ts +160 -0
  149. package/dist/tui/scene.d.ts.map +1 -0
  150. package/dist/tui/scene.js +424 -0
  151. package/dist/tui/scene.js.map +1 -0
  152. package/dist/tui/screens/config.d.ts +3 -0
  153. package/dist/tui/screens/config.d.ts.map +1 -0
  154. package/dist/tui/screens/config.js +406 -0
  155. package/dist/tui/screens/config.js.map +1 -0
  156. package/dist/tui/screens/dashboard.d.ts +3 -0
  157. package/dist/tui/screens/dashboard.d.ts.map +1 -0
  158. package/dist/tui/screens/dashboard.js +176 -0
  159. package/dist/tui/screens/dashboard.js.map +1 -0
  160. package/dist/tui/screens/devices.d.ts +3 -0
  161. package/dist/tui/screens/devices.d.ts.map +1 -0
  162. package/dist/tui/screens/devices.js +297 -0
  163. package/dist/tui/screens/devices.js.map +1 -0
  164. package/dist/tui/screens/incidents.d.ts +4 -0
  165. package/dist/tui/screens/incidents.d.ts.map +1 -0
  166. package/dist/tui/screens/incidents.js +304 -0
  167. package/dist/tui/screens/incidents.js.map +1 -0
  168. package/dist/tui/screens/setup.d.ts +3 -0
  169. package/dist/tui/screens/setup.d.ts.map +1 -0
  170. package/dist/tui/screens/setup.js +299 -0
  171. package/dist/tui/screens/setup.js.map +1 -0
  172. package/dist/tui/screens/spaces.d.ts +7 -0
  173. package/dist/tui/screens/spaces.d.ts.map +1 -0
  174. package/dist/tui/screens/spaces.js +422 -0
  175. package/dist/tui/screens/spaces.js.map +1 -0
  176. package/dist/tui/screens/tickets.d.ts +9 -0
  177. package/dist/tui/screens/tickets.d.ts.map +1 -0
  178. package/dist/tui/screens/tickets.js +418 -0
  179. package/dist/tui/screens/tickets.js.map +1 -0
  180. package/dist/tui/serialize.d.ts +31 -0
  181. package/dist/tui/serialize.d.ts.map +1 -0
  182. package/dist/tui/serialize.js +183 -0
  183. package/dist/tui/serialize.js.map +1 -0
  184. package/dist/tui/table-format.d.ts +11 -0
  185. package/dist/tui/table-format.d.ts.map +1 -0
  186. package/dist/tui/table-format.js +77 -0
  187. package/dist/tui/table-format.js.map +1 -0
  188. package/dist/tui/tabs.d.ts +4 -0
  189. package/dist/tui/tabs.d.ts.map +1 -0
  190. package/dist/tui/tabs.js +13 -0
  191. package/dist/tui/tabs.js.map +1 -0
  192. package/dist/tui/types.d.ts +37 -0
  193. package/dist/tui/types.d.ts.map +1 -0
  194. package/dist/tui/types.js +3 -0
  195. package/dist/tui/types.js.map +1 -0
  196. package/dist/types/client.d.ts +54 -0
  197. package/dist/types/client.d.ts.map +1 -0
  198. package/dist/types/client.js +3 -0
  199. package/dist/types/client.js.map +1 -0
  200. package/dist/types/endpoints.d.ts +29 -0
  201. package/dist/types/endpoints.d.ts.map +1 -0
  202. package/dist/types/endpoints.js +3 -0
  203. package/dist/types/endpoints.js.map +1 -0
  204. package/dist/types/profile.d.ts +29 -0
  205. package/dist/types/profile.d.ts.map +1 -0
  206. package/dist/types/profile.js +3 -0
  207. package/dist/types/profile.js.map +1 -0
  208. package/dist/utils/config-dir.d.ts +2 -0
  209. package/dist/utils/config-dir.d.ts.map +1 -0
  210. package/dist/utils/config-dir.js +23 -0
  211. package/dist/utils/config-dir.js.map +1 -0
  212. package/dist/utils/error-format.d.ts +4 -0
  213. package/dist/utils/error-format.d.ts.map +1 -0
  214. package/dist/utils/error-format.js +34 -0
  215. package/dist/utils/error-format.js.map +1 -0
  216. package/dist/utils/install-skills.d.ts +38 -0
  217. package/dist/utils/install-skills.d.ts.map +1 -0
  218. package/dist/utils/install-skills.js +117 -0
  219. package/dist/utils/install-skills.js.map +1 -0
  220. package/dist/utils/json-output.d.ts +6 -0
  221. package/dist/utils/json-output.d.ts.map +1 -0
  222. package/dist/utils/json-output.js +30 -0
  223. package/dist/utils/json-output.js.map +1 -0
  224. package/dist/utils/json.d.ts +4 -0
  225. package/dist/utils/json.d.ts.map +1 -0
  226. package/dist/utils/json.js +36 -0
  227. package/dist/utils/json.js.map +1 -0
  228. package/dist/utils/version.d.ts +2 -0
  229. package/dist/utils/version.d.ts.map +1 -0
  230. package/dist/utils/version.js +28 -0
  231. package/dist/utils/version.js.map +1 -0
  232. package/dist/workflows/fleet-insights.d.ts +122 -0
  233. package/dist/workflows/fleet-insights.d.ts.map +1 -0
  234. package/dist/workflows/fleet-insights.js +938 -0
  235. package/dist/workflows/fleet-insights.js.map +1 -0
  236. package/docs/schemas/call-envelope.v1.schema.json +140 -0
  237. package/docs/schemas/headless-frame.v1.schema.json +159 -0
  238. package/docs/schemas/inspect-deep-dive.v1.schema.json +251 -0
  239. package/docs/schemas/inspect-fleet.v1.schema.json +111 -0
  240. package/docs/schemas/report.v1.schema.json +39 -0
  241. package/package.json +75 -0
  242. package/skills/xyte-cli/SKILL.md +181 -0
  243. package/skills/xyte-cli/agents/openai.yaml +4 -0
  244. package/skills/xyte-cli/references/endpoints.md +106 -0
  245. package/skills/xyte-cli/references/headless-contract.md +96 -0
  246. package/skills/xyte-cli/references/tui-flows.md +126 -0
  247. package/skills/xyte-cli/scripts/check_headless.sh +83 -0
  248. package/skills/xyte-cli/scripts/endpoint_filters_report.sh +33 -0
  249. package/skills/xyte-cli/scripts/run_xyte_cli.sh +12 -0
  250. package/skills/xyte-cli/scripts/validate_agent_contracts.sh +72 -0
  251. package/skills/xyte-cli/scripts/validate_with_schema.js +30 -0
@@ -0,0 +1,938 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectFleetSnapshot = collectFleetSnapshot;
7
+ exports.buildFleetInspect = buildFleetInspect;
8
+ exports.formatFleetInspectAscii = formatFleetInspectAscii;
9
+ exports.buildDeepDive = buildDeepDive;
10
+ exports.formatDeepDiveAscii = formatDeepDiveAscii;
11
+ exports.formatDeepDiveMarkdown = formatDeepDiveMarkdown;
12
+ exports.formatUtcForReport = formatUtcForReport;
13
+ exports.getWindowFocus = getWindowFocus;
14
+ exports.generateFleetReport = generateFleetReport;
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ const pdfkit_1 = __importDefault(require("pdfkit"));
18
+ const data_loaders_1 = require("../tui/data-loaders");
19
+ const versions_1 = require("../contracts/versions");
20
+ const tracing_1 = require("../observability/tracing");
21
+ function asRecord(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ return {};
24
+ }
25
+ return value;
26
+ }
27
+ function toCounter(items) {
28
+ const counter = {};
29
+ for (const item of items) {
30
+ counter[item] = (counter[item] ?? 0) + 1;
31
+ }
32
+ return counter;
33
+ }
34
+ function pct(count, total) {
35
+ if (!total) {
36
+ return 0;
37
+ }
38
+ return Number(((count * 100) / total).toFixed(1));
39
+ }
40
+ function parseTimestamp(value) {
41
+ if (typeof value !== 'string' || !value.trim()) {
42
+ return undefined;
43
+ }
44
+ const trimmed = value.trim();
45
+ const normalized = trimmed.replace(/\s+/, 'T');
46
+ const parts = normalized.match(/^(\d{4}-\d{2}-\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?(Z|[+-]\d{2}(?::?\d{2})?)?$/i);
47
+ if (parts) {
48
+ const date = parts[1];
49
+ const hour = parts[2] ?? '00';
50
+ const minute = parts[3] ?? '00';
51
+ const second = parts[4] ?? '00';
52
+ const fraction = parts[5] ? `.${parts[5].slice(0, 3).padEnd(3, '0')}` : '';
53
+ const zoneRaw = parts[6] ?? 'Z';
54
+ const zone = /^[+-]\d{4}$/.test(zoneRaw)
55
+ ? `${zoneRaw.slice(0, 3)}:${zoneRaw.slice(3)}`
56
+ : /^[+-]\d{2}$/.test(zoneRaw)
57
+ ? `${zoneRaw}:00`
58
+ : zoneRaw;
59
+ const iso = `${date}T${hour}:${minute}:${second}${fraction}${zone}`;
60
+ const parsedIso = new Date(iso);
61
+ if (!Number.isNaN(parsedIso.getTime())) {
62
+ return parsedIso;
63
+ }
64
+ }
65
+ // Treat timezone-naive ISO timestamps as UTC for deterministic reporting.
66
+ if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed) && !/(Z|[+-]\d{2}(?::?\d{2})?)$/i.test(trimmed)) {
67
+ const asUtc = new Date(`${trimmed}Z`);
68
+ if (!Number.isNaN(asUtc.getTime())) {
69
+ return asUtc;
70
+ }
71
+ }
72
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
73
+ const asDateUtc = new Date(`${trimmed}T00:00:00Z`);
74
+ if (!Number.isNaN(asDateUtc.getTime())) {
75
+ return asDateUtc;
76
+ }
77
+ }
78
+ const direct = new Date(trimmed);
79
+ if (!Number.isNaN(direct.getTime())) {
80
+ return direct;
81
+ }
82
+ return undefined;
83
+ }
84
+ function ageHours(createdAt) {
85
+ const parsed = parseTimestamp(createdAt);
86
+ if (!parsed) {
87
+ return 0;
88
+ }
89
+ const now = Date.now();
90
+ return Math.max(0, Math.round((now - parsed.getTime()) / 3_600_000));
91
+ }
92
+ function topEntries(counter, limit = 10) {
93
+ return Object.entries(counter)
94
+ .sort((a, b) => {
95
+ if (b[1] !== a[1]) {
96
+ return b[1] - a[1];
97
+ }
98
+ return a[0].localeCompare(b[0]);
99
+ })
100
+ .slice(0, limit);
101
+ }
102
+ function identifier(value) {
103
+ if (value === undefined || value === null) {
104
+ return 'n/a';
105
+ }
106
+ return String(value);
107
+ }
108
+ function safeSpacePath(value) {
109
+ return identifier(value?.space_tree_path_name ?? value?.space?.full_path ?? value?.space?.name ?? value?.space_id ?? 'unknown');
110
+ }
111
+ function safeDeviceName(value) {
112
+ return identifier(value?.device_name ?? value?.name ?? value?.device?.name ?? value?.device_id ?? 'unknown');
113
+ }
114
+ function redactSensitive(value, includeSensitive) {
115
+ if (includeSensitive || value === 'n/a') {
116
+ return value;
117
+ }
118
+ if (value.length <= 8) {
119
+ return '***';
120
+ }
121
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
122
+ }
123
+ async function loadAllDevices(client, tenantId) {
124
+ const perPage = 100;
125
+ async function fetchAll(fetcher) {
126
+ const all = [];
127
+ for (let page = 1; page <= 50; page += 1) {
128
+ const raw = await fetcher({ page, per_page: perPage });
129
+ const pageItems = (0, data_loaders_1.extractArray)(raw, ['devices', 'data', 'items']);
130
+ if (!pageItems.length)
131
+ break;
132
+ all.push(...pageItems);
133
+ if (pageItems.length < perPage)
134
+ break;
135
+ }
136
+ return all;
137
+ }
138
+ try {
139
+ const devices = await fetchAll((q) => client.organization.getDevices({ tenantId, query: q }));
140
+ if (devices.length > 0)
141
+ return devices;
142
+ }
143
+ catch { /* fall through to partner */ }
144
+ try {
145
+ const devices = await fetchAll((q) => client.partner.getDevices({ tenantId, query: q }));
146
+ if (devices.length > 0)
147
+ return devices;
148
+ }
149
+ catch { /* fall through to unpaginated */ }
150
+ const single = await client.organization.getDevices({ tenantId }).catch(() => client.partner.getDevices({ tenantId }));
151
+ return (0, data_loaders_1.extractArray)(single, ['devices', 'data', 'items']);
152
+ }
153
+ async function loadAllSpaces(client, tenantId) {
154
+ const perPage = 100;
155
+ const all = [];
156
+ for (let page = 1; page <= 50; page += 1) {
157
+ const raw = await client.organization.getSpaces({
158
+ tenantId,
159
+ query: { page, per_page: perPage }
160
+ });
161
+ const pageItems = (0, data_loaders_1.extractArray)(raw, ['spaces', 'data', 'items']);
162
+ if (!pageItems.length) {
163
+ break;
164
+ }
165
+ all.push(...pageItems);
166
+ if (pageItems.length < perPage) {
167
+ break;
168
+ }
169
+ }
170
+ if (all.length > 0) {
171
+ return all;
172
+ }
173
+ const single = await client.organization.getSpaces({ tenantId });
174
+ return (0, data_loaders_1.extractArray)(single, ['spaces', 'data', 'items']);
175
+ }
176
+ async function collectFleetSnapshot(client, tenantId) {
177
+ return (0, tracing_1.withSpan)('xyte.inspect.collect_snapshot', { 'xyte.tenant.id': tenantId }, async () => {
178
+ const [devices, spaces, incidentsRaw, orgTicketsRaw, partnerTicketsRaw] = await Promise.all([
179
+ loadAllDevices(client, tenantId),
180
+ loadAllSpaces(client, tenantId),
181
+ client.organization.getIncidents({ tenantId }),
182
+ client.organization.getTickets({ tenantId }).catch(() => ({ items: [] })),
183
+ client.partner.getTickets({ tenantId }).catch(() => ({ items: [] }))
184
+ ]);
185
+ const incidents = (0, data_loaders_1.extractArray)(incidentsRaw, ['incidents', 'data', 'items']);
186
+ const orgTickets = (0, data_loaders_1.extractArray)(orgTicketsRaw, ['tickets', 'data', 'items']);
187
+ const partnerTickets = (0, data_loaders_1.extractArray)(partnerTicketsRaw, ['tickets', 'data', 'items']);
188
+ const tickets = [...orgTickets, ...partnerTickets];
189
+ const stableSort = (items) => items.slice().sort((a, b) => identifier(a?.id ?? a?.name ?? a?.title).localeCompare(identifier(b?.id ?? b?.name ?? b?.title)));
190
+ return {
191
+ generatedAtUtc: new Date().toISOString(),
192
+ tenantId,
193
+ devices: stableSort(devices),
194
+ spaces: stableSort(spaces),
195
+ incidents: stableSort(incidents),
196
+ tickets: stableSort(tickets)
197
+ };
198
+ });
199
+ }
200
+ function buildFleetInspect(snapshot) {
201
+ const deviceStatus = toCounter(snapshot.devices.map((item) => identifier(item?.status ?? 'unknown')));
202
+ const incidentStatus = toCounter(snapshot.incidents.map((item) => identifier(item?.status ?? 'unknown')));
203
+ const ticketStatus = toCounter(snapshot.tickets.map((item) => identifier(item?.status ?? 'unknown')));
204
+ const spaceTypes = toCounter(snapshot.spaces.map((item) => identifier(item?.space_type ?? 'unknown')));
205
+ const offlineDevices = deviceStatus.offline ?? 0;
206
+ const activeIncidents = incidentStatus.active ?? 0;
207
+ const openTickets = ticketStatus.open ?? 0;
208
+ return {
209
+ schemaVersion: versions_1.INSPECT_FLEET_SCHEMA_VERSION,
210
+ generatedAtUtc: snapshot.generatedAtUtc,
211
+ tenantId: snapshot.tenantId,
212
+ totals: {
213
+ devices: snapshot.devices.length,
214
+ spaces: snapshot.spaces.length,
215
+ incidents: snapshot.incidents.length,
216
+ tickets: snapshot.tickets.length
217
+ },
218
+ status: {
219
+ devices: deviceStatus,
220
+ incidents: incidentStatus,
221
+ tickets: ticketStatus,
222
+ spaces: spaceTypes
223
+ },
224
+ highlights: {
225
+ offlineDevices,
226
+ offlinePct: pct(offlineDevices, snapshot.devices.length),
227
+ activeIncidents,
228
+ activeIncidentPct: pct(activeIncidents, snapshot.incidents.length),
229
+ openTickets
230
+ }
231
+ };
232
+ }
233
+ function asciiBar(label, count, total, width = 30) {
234
+ const share = total > 0 ? count / total : 0;
235
+ const filled = Math.min(width, Math.max(0, Math.round(share * width)));
236
+ const bar = `${'#'.repeat(filled)}${' '.repeat(width - filled)}`;
237
+ return `${label.padEnd(12)} ${String(count).padStart(4)} |${bar}| ${String((share * 100).toFixed(1)).padStart(5)}%`;
238
+ }
239
+ function formatFleetInspectAscii(result) {
240
+ return [
241
+ `Fleet Inspect Snapshot (${result.tenantId})`,
242
+ `Generated: ${result.generatedAtUtc}`,
243
+ '',
244
+ 'DEVICES',
245
+ asciiBar('offline', result.status.devices.offline ?? 0, result.totals.devices),
246
+ asciiBar('online', result.status.devices.online ?? 0, result.totals.devices),
247
+ asciiBar('other', result.totals.devices - (result.status.devices.offline ?? 0) - (result.status.devices.online ?? 0), result.totals.devices),
248
+ '',
249
+ 'INCIDENTS',
250
+ asciiBar('active', result.status.incidents.active ?? 0, result.totals.incidents),
251
+ asciiBar('closed', result.status.incidents.closed ?? 0, result.totals.incidents),
252
+ '',
253
+ 'TICKETS',
254
+ asciiBar('open', result.status.tickets.open ?? 0, Math.max(1, result.totals.tickets)),
255
+ '',
256
+ `Highlights: offline=${result.highlights.offlinePct}% active_incidents=${result.highlights.activeIncidentPct}% open_tickets=${result.highlights.openTickets}`
257
+ ].join('\n');
258
+ }
259
+ function buildDeepDive(snapshot, windowHours = 24) {
260
+ const offlineDevices = snapshot.devices.filter((item) => identifier(item?.status) === 'offline');
261
+ const activeIncidents = snapshot.incidents.filter((item) => identifier(item?.status) === 'active');
262
+ const openTickets = snapshot.tickets.filter((item) => identifier(item?.status) === 'open');
263
+ const offlineBySpace = toCounter(offlineDevices.map((item) => safeSpacePath(item)));
264
+ const incidentsByDevice = toCounter(snapshot.incidents.map((item) => safeDeviceName(item)));
265
+ const activeByDevice = toCounter(activeIncidents.map((item) => safeDeviceName(item)));
266
+ const recentIncidents = snapshot.incidents.filter((item) => ageHours(item?.created_at) <= windowHours);
267
+ const recentSpace = toCounter(recentIncidents.map((item) => safeSpacePath(item)));
268
+ const recentDevice = toCounter(recentIncidents.map((item) => safeDeviceName(item)));
269
+ const activeDeviceIds = new Set(activeIncidents.map((item) => identifier(item?.device_id ?? item?.device?.id)));
270
+ const overlapDevices = new Set(openTickets.map((item) => identifier(item?.device_id)).filter((id) => activeDeviceIds.has(id)));
271
+ const mismatches = snapshot.devices
272
+ .map((item) => {
273
+ const nestedState = asRecord(item?.state).status;
274
+ if (nestedState === undefined) {
275
+ return undefined;
276
+ }
277
+ const topLevel = identifier(item?.status);
278
+ const nested = identifier(nestedState);
279
+ if (topLevel === nested) {
280
+ return undefined;
281
+ }
282
+ return {
283
+ device: safeDeviceName(item),
284
+ status: topLevel,
285
+ stateStatus: nested,
286
+ lastSeen: identifier(item?.last_seen_at),
287
+ space: safeSpacePath(item)
288
+ };
289
+ })
290
+ .filter((item) => Boolean(item))
291
+ .sort((a, b) => a.device.localeCompare(b.device));
292
+ const topOfflineSpaces = topEntries(offlineBySpace, 10).map(([space, count]) => ({
293
+ space,
294
+ offlineDevices: count,
295
+ shareOfOfflinePct: pct(count, offlineDevices.length)
296
+ }));
297
+ const topIncidentDevices = topEntries(incidentsByDevice, 10).map(([device, count]) => ({
298
+ device,
299
+ incidentCount: count,
300
+ activeIncidents: activeByDevice[device] ?? 0
301
+ }));
302
+ const activeIncidentAging = activeIncidents
303
+ .map((item) => ({
304
+ device: safeDeviceName(item),
305
+ space: safeSpacePath(item),
306
+ ageHours: ageHours(item?.created_at),
307
+ createdAtUtc: identifier(item?.created_at)
308
+ }))
309
+ .sort((a, b) => b.ageHours - a.ageHours);
310
+ const oldestOpenTickets = openTickets
311
+ .map((item) => ({
312
+ ticketId: identifier(item?.id),
313
+ title: identifier(item?.title ?? item?.subject),
314
+ ageHours: ageHours(item?.created_at),
315
+ deviceId: identifier(item?.device_id),
316
+ createdAtUtc: identifier(item?.created_at)
317
+ }))
318
+ .sort((a, b) => b.ageHours - a.ageHours)
319
+ .slice(0, 20);
320
+ const summary = [
321
+ `Devices: ${snapshot.devices.length} total, ${offlineDevices.length} offline (${pct(offlineDevices.length, snapshot.devices.length)}%).`,
322
+ `Incidents: ${snapshot.incidents.length} total, ${activeIncidents.length} active (${pct(activeIncidents.length, snapshot.incidents.length)}%).`,
323
+ `Tickets: ${snapshot.tickets.length} total, ${openTickets.length} open.`,
324
+ `${windowHours}h churn: ${recentIncidents.length} incidents across ${Object.keys(recentDevice).length} devices and ${Object.keys(recentSpace).length} spaces.`,
325
+ `Data quality: ${mismatches.length} status mismatches detected.`
326
+ ];
327
+ return {
328
+ schemaVersion: versions_1.INSPECT_DEEP_DIVE_SCHEMA_VERSION,
329
+ generatedAtUtc: snapshot.generatedAtUtc,
330
+ tenantId: snapshot.tenantId,
331
+ windowHours,
332
+ summary,
333
+ topOfflineSpaces,
334
+ topIncidentDevices,
335
+ activeIncidentAging,
336
+ churn24h: {
337
+ incidents: recentIncidents.length,
338
+ devices: Object.keys(recentDevice).length,
339
+ spaces: Object.keys(recentSpace).length,
340
+ bySpace: topEntries(recentSpace, 10).map(([space, incidents]) => ({ space, incidents })),
341
+ byDevice: topEntries(recentDevice, 10).map(([device, incidents]) => ({ device, incidents }))
342
+ },
343
+ ticketPosture: {
344
+ openTickets: openTickets.length,
345
+ overlappingActiveIncidentDevices: overlapDevices.size,
346
+ oldestOpenTickets
347
+ },
348
+ dataQuality: {
349
+ statusMismatches: mismatches
350
+ }
351
+ };
352
+ }
353
+ function formatDeepDiveAscii(result) {
354
+ const lines = [];
355
+ lines.push(`Deep Dive (${result.tenantId})`);
356
+ lines.push(`Generated: ${result.generatedAtUtc}`);
357
+ lines.push('');
358
+ lines.push('SUMMARY');
359
+ result.summary.forEach((line) => lines.push(`- ${line}`));
360
+ lines.push('');
361
+ lines.push('TOP OFFLINE SPACES');
362
+ result.topOfflineSpaces.forEach((row) => lines.push(`${row.space} | offline=${row.offlineDevices} | share=${row.shareOfOfflinePct}%`));
363
+ lines.push('');
364
+ lines.push('TOP INCIDENT DEVICES');
365
+ result.topIncidentDevices.forEach((row) => lines.push(`${row.device} | incidents=${row.incidentCount} | active=${row.activeIncidents}`));
366
+ lines.push('');
367
+ lines.push(`24H CHURN: incidents=${result.churn24h.incidents} devices=${result.churn24h.devices} spaces=${result.churn24h.spaces}`);
368
+ result.churn24h.bySpace.forEach((row) => lines.push(`space: ${row.space} -> ${row.incidents}`));
369
+ lines.push('');
370
+ lines.push(`OPEN TICKETS: ${result.ticketPosture.openTickets}`);
371
+ lines.push(`OVERLAP DEVICES: ${result.ticketPosture.overlappingActiveIncidentDevices}`);
372
+ return lines.join('\n');
373
+ }
374
+ function formatDeepDiveMarkdown(result, includeSensitive = false) {
375
+ const markdown = [];
376
+ markdown.push('# Xyte Fleet Deep Dive');
377
+ markdown.push('');
378
+ markdown.push(`- Tenant: \`${result.tenantId}\``);
379
+ markdown.push(`- Generated: \`${result.generatedAtUtc}\``);
380
+ markdown.push(`- Window: \`${result.windowHours}h\``);
381
+ markdown.push('');
382
+ markdown.push('## Summary');
383
+ markdown.push('');
384
+ result.summary.forEach((line) => markdown.push(`- ${line}`));
385
+ markdown.push('');
386
+ markdown.push('## Top Offline Spaces');
387
+ markdown.push('');
388
+ markdown.push('| Space | Offline Devices | Share |');
389
+ markdown.push('| --- | ---: | ---: |');
390
+ result.topOfflineSpaces.forEach((row) => markdown.push(`| ${row.space} | ${row.offlineDevices} | ${row.shareOfOfflinePct}% |`));
391
+ markdown.push('');
392
+ markdown.push('## Top Devices by Incident Volume');
393
+ markdown.push('');
394
+ markdown.push('| Device | Incidents | Active |');
395
+ markdown.push('| --- | ---: | ---: |');
396
+ result.topIncidentDevices.forEach((row) => markdown.push(`| ${row.device} | ${row.incidentCount} | ${row.activeIncidents} |`));
397
+ markdown.push('');
398
+ markdown.push(`## ${result.windowHours}-Hour Churn`);
399
+ markdown.push('');
400
+ markdown.push(`Incidents: **${result.churn24h.incidents}**, devices: **${result.churn24h.devices}**, spaces: **${result.churn24h.spaces}**.`);
401
+ markdown.push('');
402
+ markdown.push('| Space | Incidents |');
403
+ markdown.push('| --- | ---: |');
404
+ result.churn24h.bySpace.forEach((row) => markdown.push(`| ${row.space} | ${row.incidents} |`));
405
+ markdown.push('');
406
+ markdown.push('| Device | Incidents |');
407
+ markdown.push('| --- | ---: |');
408
+ result.churn24h.byDevice.forEach((row) => markdown.push(`| ${row.device} | ${row.incidents} |`));
409
+ markdown.push('');
410
+ markdown.push('## Ticket Posture');
411
+ markdown.push('');
412
+ markdown.push(`- Open tickets: **${result.ticketPosture.openTickets}**`);
413
+ markdown.push(`- Overlapping active-incident devices: **${result.ticketPosture.overlappingActiveIncidentDevices}**`);
414
+ markdown.push('');
415
+ markdown.push('| Ticket ID | Title | Age (h) | Device ID | Created At |');
416
+ markdown.push('| --- | --- | ---: | --- | --- |');
417
+ result.ticketPosture.oldestOpenTickets.slice(0, 10).forEach((row) => {
418
+ markdown.push(`| ${redactSensitive(row.ticketId, includeSensitive)} | ${row.title} | ${row.ageHours} | ${redactSensitive(row.deviceId, includeSensitive)} | ${row.createdAtUtc} |`);
419
+ });
420
+ markdown.push('');
421
+ markdown.push('## Data Quality');
422
+ markdown.push('');
423
+ if (!result.dataQuality.statusMismatches.length) {
424
+ markdown.push('No status mismatches detected.');
425
+ }
426
+ else {
427
+ markdown.push('| Device | Status | state.status | Last Seen | Space |');
428
+ markdown.push('| --- | --- | --- | --- | --- |');
429
+ result.dataQuality.statusMismatches.forEach((row) => markdown.push(`| ${row.device} | ${row.status} | ${row.stateStatus} | ${row.lastSeen} | ${row.space} |`));
430
+ }
431
+ return markdown.join('\n');
432
+ }
433
+ function ensureDir(filePath) {
434
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, node_path_1.resolve)(filePath)), { recursive: true });
435
+ }
436
+ const PAGE_MARGIN_X = 46;
437
+ const PAGE_MARGIN_Y = 42;
438
+ const HEADER_TOP = 18;
439
+ const HEADER_HEIGHT = 68;
440
+ const FOOTER_HEIGHT = 22;
441
+ const CONTENT_TOP = HEADER_TOP + HEADER_HEIGHT + 16;
442
+ const SPACE_SM = 8;
443
+ const SPACE_MD = 12;
444
+ const SPACE_LG = 18;
445
+ const SPACE_XL = 24;
446
+ const FONT_H1 = 18;
447
+ const FONT_H2 = 13;
448
+ const FONT_BODY = 10;
449
+ const FONT_CAPTION = 9;
450
+ const TABLE_ROW_MIN = 22;
451
+ const TABLE_ROW_MAX = 220;
452
+ const TABLE_CELL_PAD_X = 6;
453
+ const TABLE_CELL_PAD_Y = 5;
454
+ function resolveLogoPath() {
455
+ const candidates = [
456
+ (0, node_path_1.resolve)(process.cwd(), 'assets/xyte-logo.png'),
457
+ (0, node_path_1.resolve)(__dirname, '../../assets/xyte-logo.png'),
458
+ (0, node_path_1.resolve)(__dirname, '../../../assets/xyte-logo.png')
459
+ ];
460
+ return candidates.find((candidate) => (0, node_fs_1.existsSync)(candidate));
461
+ }
462
+ function formatTwoDigits(value) {
463
+ return String(value).padStart(2, '0');
464
+ }
465
+ function formatUtcForReport(value) {
466
+ const parsed = parseTimestamp(value);
467
+ if (!parsed) {
468
+ return identifier(value);
469
+ }
470
+ const y = parsed.getUTCFullYear();
471
+ const m = formatTwoDigits(parsed.getUTCMonth() + 1);
472
+ const d = formatTwoDigits(parsed.getUTCDate());
473
+ const hh = formatTwoDigits(parsed.getUTCHours());
474
+ const mm = formatTwoDigits(parsed.getUTCMinutes());
475
+ return `${y}-${m}-${d} ${hh}:${mm} UTC`;
476
+ }
477
+ function getWindowFocus(windowHours) {
478
+ if (windowHours <= 24) {
479
+ return {
480
+ label: 'Immediate churn',
481
+ detail: 'Prioritize active incident containment and hot spaces in the last day.',
482
+ accent: '#B45309'
483
+ };
484
+ }
485
+ if (windowHours <= 72) {
486
+ return {
487
+ label: 'Short-term Trend',
488
+ detail: 'Track repeat offenders and stabilize recurring high-churn spaces.',
489
+ accent: '#1D4ED8'
490
+ };
491
+ }
492
+ return {
493
+ label: 'Weekly concentration',
494
+ detail: 'Focus on sustained incident concentration and structural remediation.',
495
+ accent: '#166534'
496
+ };
497
+ }
498
+ function resetCursor(doc) {
499
+ doc.x = doc.page.margins.left;
500
+ doc.y = Math.max(doc.y, CONTENT_TOP);
501
+ }
502
+ function drawPdfHeader(doc, ctx) {
503
+ const left = doc.page.margins.left;
504
+ const right = doc.page.width - doc.page.margins.right;
505
+ const bandTop = HEADER_TOP;
506
+ const bandHeight = HEADER_HEIGHT;
507
+ doc.save();
508
+ doc.roundedRect(left, bandTop, right - left, bandHeight, 8).fillAndStroke('#E8F0FC', '#C2D5F3');
509
+ doc.restore();
510
+ if (ctx.logoPath) {
511
+ try {
512
+ doc.image(ctx.logoPath, left + 12, bandTop + 16, { fit: [110, 34] });
513
+ }
514
+ catch {
515
+ doc.font('Helvetica-Bold').fontSize(36).fillColor('#1459A6').text('XYTE', left + 12, bandTop + 12);
516
+ }
517
+ }
518
+ else {
519
+ doc.font('Helvetica-Bold').fontSize(36).fillColor('#1459A6').text('XYTE', left + 12, bandTop + 12);
520
+ }
521
+ doc
522
+ .font('Helvetica-Bold')
523
+ .fontSize(FONT_H1)
524
+ .fillColor('#1A2332')
525
+ .text('Fleet Findings Report', left + 146, bandTop + 14, { width: right - left - 250, align: 'left' });
526
+ doc
527
+ .font('Helvetica')
528
+ .fontSize(FONT_BODY)
529
+ .fillColor('#415067')
530
+ .text(`Tenant: ${ctx.tenantId}`, left + 146, bandTop + 37, { width: right - left - 250, align: 'left' })
531
+ .text(`Generated: ${formatUtcForReport(ctx.generatedAtUtc)}`, left + 146, bandTop + 51, { width: right - left - 250, align: 'left' });
532
+ const badgeWidth = 165;
533
+ const badgeHeight = 28;
534
+ const badgeX = right - badgeWidth - 12;
535
+ const badgeY = bandTop + 20;
536
+ doc.save();
537
+ doc.roundedRect(badgeX, badgeY, badgeWidth, badgeHeight, 14).fill(ctx.windowFocus.accent);
538
+ doc.restore();
539
+ doc.font('Helvetica-Bold').fontSize(FONT_CAPTION).fillColor('#FFFFFF').text(`${ctx.windowHours}h • ${ctx.windowFocus.label}`, badgeX + 12, badgeY + 9, { width: badgeWidth - 24, align: 'center' });
540
+ }
541
+ function drawPdfFooter(doc, ctx, pageNumber, pageCount) {
542
+ const y = doc.page.height - doc.page.margins.bottom - FOOTER_HEIGHT + 10;
543
+ const left = doc.page.margins.left;
544
+ const right = doc.page.width - doc.page.margins.right;
545
+ doc.save();
546
+ doc.moveTo(left, y - 6).lineTo(right, y - 6).lineWidth(0.6).strokeColor('#D5DEE9').stroke();
547
+ doc.restore();
548
+ doc.font('Helvetica').fontSize(FONT_CAPTION).fillColor('#5B687B').text('Xyte Fleet Findings Report', left, y, { width: 220, align: 'left' });
549
+ doc.text(`${ctx.windowHours}h window`, left + 220, y, { width: 120, align: 'center' });
550
+ doc.text(`Page ${pageNumber} of ${pageCount}`, right - 120, y, { width: 120, align: 'right' });
551
+ }
552
+ function startReportPage(doc, ctx) {
553
+ doc.addPage();
554
+ drawPdfHeader(doc, ctx);
555
+ resetCursor(doc);
556
+ }
557
+ function ensurePageSpace(doc, ctx, minHeight) {
558
+ resetCursor(doc);
559
+ const bottom = doc.page.height - doc.page.margins.bottom - FOOTER_HEIGHT - SPACE_SM;
560
+ if (doc.y + minHeight <= bottom) {
561
+ return;
562
+ }
563
+ startReportPage(doc, ctx);
564
+ }
565
+ function drawSectionTitle(doc, ctx, title) {
566
+ ensurePageSpace(doc, ctx, 30);
567
+ resetCursor(doc);
568
+ doc.moveDown(0.2);
569
+ doc.font('Helvetica-Bold').fontSize(FONT_H2).fillColor('#182433').text(title, {
570
+ width: doc.page.width - doc.page.margins.left - doc.page.margins.right
571
+ });
572
+ const y = doc.y + 2;
573
+ doc.save();
574
+ doc.moveTo(doc.page.margins.left, y).lineTo(doc.page.width - doc.page.margins.right, y).lineWidth(0.8).strokeColor('#D4DEE8').stroke();
575
+ doc.restore();
576
+ doc.moveDown(0.2);
577
+ }
578
+ function drawWindowFocusStrip(doc, ctx) {
579
+ ensurePageSpace(doc, ctx, 56);
580
+ resetCursor(doc);
581
+ const x = doc.page.margins.left;
582
+ const y = doc.y;
583
+ const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
584
+ const height = 48;
585
+ doc.save();
586
+ doc.roundedRect(x, y, width, height, 7).fillAndStroke('#F3F7FC', '#D7E3F2');
587
+ doc.restore();
588
+ doc.font('Helvetica-Bold').fontSize(FONT_BODY).fillColor(ctx.windowFocus.accent).text('Window Focus', x + 12, y + 10);
589
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#243447').text(ctx.windowFocus.detail, x + 110, y + 10, {
590
+ width: width - 122
591
+ });
592
+ doc.y = y + height + SPACE_MD;
593
+ }
594
+ function drawKpiGrid(doc, ctx, cards) {
595
+ ensurePageSpace(doc, ctx, 106);
596
+ resetCursor(doc);
597
+ const startX = doc.page.margins.left;
598
+ const topY = doc.y;
599
+ const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
600
+ const gap = SPACE_SM;
601
+ const cardWidth = Math.floor((width - gap * 3) / 4);
602
+ const cardHeight = 84;
603
+ cards.slice(0, 4).forEach((card, index) => {
604
+ const x = startX + index * (cardWidth + gap);
605
+ const tone = card.tone === 'bad'
606
+ ? { bg: '#FDEBEC', border: '#F7C4C7', value: '#A2282F' }
607
+ : card.tone === 'warn'
608
+ ? { bg: '#FFF6E8', border: '#F7D9A6', value: '#9C5F08' }
609
+ : { bg: '#EEF6FF', border: '#C6E0FF', value: '#1459A6' };
610
+ doc.save();
611
+ doc.roundedRect(x, topY, cardWidth, cardHeight, 8).fillAndStroke(tone.bg, tone.border);
612
+ doc.restore();
613
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#4B5563').text(card.label, x + 10, topY + 13, {
614
+ width: cardWidth - 20
615
+ });
616
+ doc.font('Helvetica-Bold').fontSize(26).fillColor(tone.value).text(card.value, x + 10, topY + 37, {
617
+ width: cardWidth - 20
618
+ });
619
+ });
620
+ doc.y = topY + cardHeight + SPACE_LG;
621
+ }
622
+ function drawKeyFindings(doc, ctx, lines) {
623
+ const findings = lines.slice(0, 4);
624
+ if (!findings.length) {
625
+ return;
626
+ }
627
+ ensurePageSpace(doc, ctx, 72);
628
+ resetCursor(doc);
629
+ const x = doc.page.margins.left;
630
+ const y = doc.y;
631
+ const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
632
+ doc.save();
633
+ doc.roundedRect(x, y, width, 56 + findings.length * 10, 8).fillAndStroke('#F8FAFD', '#DCE6F2');
634
+ doc.restore();
635
+ doc.font('Helvetica-Bold').fontSize(FONT_BODY).fillColor('#223245').text('Key Findings', x + 12, y + 10);
636
+ let cursorY = y + 26;
637
+ findings.forEach((line) => {
638
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#1F2A38').text(`- ${line}`, x + 12, cursorY, {
639
+ width: width - 24
640
+ });
641
+ cursorY += 14;
642
+ });
643
+ doc.y = y + 56 + findings.length * 10 + SPACE_LG;
644
+ }
645
+ function drawBullets(doc, ctx, lines) {
646
+ lines.forEach((line) => {
647
+ ensurePageSpace(doc, ctx, 20);
648
+ resetCursor(doc);
649
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#1F2937').text(`- ${line}`, {
650
+ width: doc.page.width - doc.page.margins.left - doc.page.margins.right
651
+ });
652
+ doc.moveDown(0.05);
653
+ });
654
+ }
655
+ function drawSpaceBars(doc, ctx, rows) {
656
+ if (!rows.length) {
657
+ return;
658
+ }
659
+ drawSectionTitle(doc, ctx, `${ctx.windowHours}h Churn Concentration (Top Spaces)`);
660
+ const chartRows = rows.slice(0, 5);
661
+ const maxValue = Math.max(...chartRows.map((row) => row.incidents), 1);
662
+ const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
663
+ const labelWidth = 285;
664
+ const valueWidth = 50;
665
+ const barWidth = pageWidth - labelWidth - valueWidth - 20;
666
+ chartRows.forEach((row) => {
667
+ ensurePageSpace(doc, ctx, 24);
668
+ resetCursor(doc);
669
+ const y = doc.y;
670
+ const x = doc.page.margins.left;
671
+ const ratio = row.incidents / maxValue;
672
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#1F2937').text(row.space, x, y + 5, {
673
+ width: labelWidth - 8,
674
+ ellipsis: true
675
+ });
676
+ doc.save();
677
+ doc.roundedRect(x + labelWidth, y + 8, barWidth, 9, 3).fill('#E6ECF5');
678
+ doc.roundedRect(x + labelWidth, y + 8, Math.max(8, barWidth * ratio), 9, 3).fill('#3B82F6');
679
+ doc.restore();
680
+ doc.font('Helvetica-Bold').fontSize(FONT_BODY).fillColor('#1F2937').text(String(row.incidents), x + labelWidth + barWidth + 8, y + 5, {
681
+ width: valueWidth,
682
+ align: 'right'
683
+ });
684
+ doc.y = y + 22;
685
+ });
686
+ doc.moveDown(0.35);
687
+ }
688
+ function normalizeColumns(columns, availableWidth) {
689
+ const total = columns.reduce((sum, column) => sum + column.width, 0);
690
+ if (Math.abs(total - availableWidth) <= 1) {
691
+ return columns;
692
+ }
693
+ if (total > availableWidth) {
694
+ const ratio = availableWidth / total;
695
+ const scaled = columns.map((column) => ({ ...column, width: Math.floor(column.width * ratio) }));
696
+ const scaledTotal = scaled.reduce((sum, column) => sum + column.width, 0);
697
+ scaled[0].width += availableWidth - scaledTotal;
698
+ return scaled;
699
+ }
700
+ const grown = columns.map((column) => ({ ...column }));
701
+ const flexIndex = grown.findIndex((column) => column.wrap !== false);
702
+ const target = flexIndex === -1 ? 0 : flexIndex;
703
+ grown[target].width += availableWidth - total;
704
+ return grown;
705
+ }
706
+ function measureTableRowHeight(doc, columns, row) {
707
+ doc.font('Helvetica').fontSize(FONT_BODY);
708
+ const lineHeight = doc.currentLineHeight();
709
+ let maxHeight = lineHeight;
710
+ row.forEach((cell, index) => {
711
+ const column = columns[index];
712
+ const innerWidth = Math.max(20, column.width - TABLE_CELL_PAD_X * 2);
713
+ if (column.wrap === false) {
714
+ maxHeight = Math.max(maxHeight, lineHeight);
715
+ return;
716
+ }
717
+ const measured = doc.heightOfString(cell, {
718
+ width: innerWidth,
719
+ align: column.align ?? 'left'
720
+ });
721
+ maxHeight = Math.max(maxHeight, measured);
722
+ });
723
+ const rowHeight = maxHeight + TABLE_CELL_PAD_Y * 2;
724
+ return Math.min(TABLE_ROW_MAX, Math.max(TABLE_ROW_MIN, rowHeight));
725
+ }
726
+ function drawTable(doc, ctx, args) {
727
+ const tableLeft = doc.page.margins.left;
728
+ const availableWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
729
+ const columns = normalizeColumns(args.columns, availableWidth);
730
+ const headerHeight = 24;
731
+ const continuationTitle = `${args.title} (cont.)`;
732
+ const drawHeader = () => {
733
+ ensurePageSpace(doc, ctx, headerHeight + 6);
734
+ resetCursor(doc);
735
+ const y = doc.y;
736
+ let x = tableLeft;
737
+ columns.forEach((column) => {
738
+ doc.save();
739
+ doc.rect(x, y, column.width, headerHeight).fillAndStroke('#E8EEF6', '#CAD7E8');
740
+ doc.restore();
741
+ doc.font('Helvetica-Bold').fontSize(FONT_BODY).fillColor('#1F2937').text(column.header, x + TABLE_CELL_PAD_X, y + 6, {
742
+ width: column.width - TABLE_CELL_PAD_X * 2,
743
+ align: column.align ?? 'left',
744
+ ellipsis: true
745
+ });
746
+ x += column.width;
747
+ });
748
+ doc.y = y + headerHeight;
749
+ };
750
+ if (!args.rows.length) {
751
+ ensurePageSpace(doc, ctx, 32 + headerHeight);
752
+ drawSectionTitle(doc, ctx, args.title);
753
+ ensurePageSpace(doc, ctx, 24);
754
+ resetCursor(doc);
755
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#475569').text(args.emptyMessage ?? 'No data available.', {
756
+ width: availableWidth
757
+ });
758
+ doc.moveDown(0.5);
759
+ return;
760
+ }
761
+ const firstRowHeight = measureTableRowHeight(doc, columns, args.rows[0]);
762
+ ensurePageSpace(doc, ctx, 34 + headerHeight + firstRowHeight);
763
+ drawSectionTitle(doc, ctx, args.title);
764
+ drawHeader();
765
+ args.rows.forEach((row) => {
766
+ const rowHeight = measureTableRowHeight(doc, columns, row);
767
+ const bottom = doc.page.height - doc.page.margins.bottom - FOOTER_HEIGHT - SPACE_SM;
768
+ if (doc.y + rowHeight > bottom) {
769
+ startReportPage(doc, ctx);
770
+ ensurePageSpace(doc, ctx, 34 + headerHeight + Math.min(rowHeight, 60));
771
+ drawSectionTitle(doc, ctx, continuationTitle);
772
+ drawHeader();
773
+ }
774
+ const y = doc.y;
775
+ let x = tableLeft;
776
+ row.forEach((cell, index) => {
777
+ const column = columns[index];
778
+ doc.save();
779
+ doc.rect(x, y, column.width, rowHeight).fillAndStroke('#FFFFFF', '#E3EAF3');
780
+ doc.restore();
781
+ doc.font('Helvetica').fontSize(FONT_BODY).fillColor('#0F172A').text(cell, x + TABLE_CELL_PAD_X, y + TABLE_CELL_PAD_Y, {
782
+ width: column.width - TABLE_CELL_PAD_X * 2,
783
+ height: rowHeight - TABLE_CELL_PAD_Y * 2,
784
+ align: column.align ?? 'left',
785
+ lineBreak: column.wrap !== false,
786
+ ellipsis: column.wrap === false
787
+ });
788
+ x += column.width;
789
+ });
790
+ doc.y = y + rowHeight;
791
+ });
792
+ doc.moveDown(0.45);
793
+ }
794
+ function renderBrandedPdfReport(deepDive, outputPath, includeSensitive) {
795
+ return new Promise((resolvePromise, rejectPromise) => {
796
+ ensureDir(outputPath);
797
+ const ctx = {
798
+ tenantId: deepDive.tenantId,
799
+ generatedAtUtc: deepDive.generatedAtUtc,
800
+ windowHours: deepDive.windowHours,
801
+ windowFocus: getWindowFocus(deepDive.windowHours),
802
+ logoPath: resolveLogoPath()
803
+ };
804
+ const doc = new pdfkit_1.default({
805
+ size: 'LETTER',
806
+ margins: {
807
+ left: PAGE_MARGIN_X,
808
+ right: PAGE_MARGIN_X,
809
+ top: PAGE_MARGIN_Y,
810
+ bottom: PAGE_MARGIN_Y
811
+ },
812
+ bufferPages: true
813
+ });
814
+ const stream = doc.pipe((0, node_fs_1.createWriteStream)(outputPath));
815
+ stream.on('finish', () => resolvePromise());
816
+ stream.on('error', (error) => rejectPromise(error));
817
+ drawPdfHeader(doc, ctx);
818
+ resetCursor(doc);
819
+ drawKpiGrid(doc, ctx, [
820
+ { label: 'Active incidents', value: String(deepDive.activeIncidentAging.length), tone: deepDive.activeIncidentAging.length > 0 ? 'warn' : 'normal' },
821
+ { label: `${deepDive.windowHours}h churn`, value: String(deepDive.churn24h.incidents), tone: deepDive.churn24h.incidents > 0 ? 'warn' : 'normal' },
822
+ { label: 'Open tickets', value: String(deepDive.ticketPosture.openTickets), tone: deepDive.ticketPosture.openTickets > 0 ? 'warn' : 'normal' },
823
+ {
824
+ label: 'Data mismatches',
825
+ value: String(deepDive.dataQuality.statusMismatches.length),
826
+ tone: deepDive.dataQuality.statusMismatches.length > 0 ? 'bad' : 'normal'
827
+ }
828
+ ]);
829
+ drawWindowFocusStrip(doc, ctx);
830
+ drawKeyFindings(doc, ctx, deepDive.summary);
831
+ drawSectionTitle(doc, ctx, 'Executive Summary');
832
+ drawBullets(doc, ctx, deepDive.summary);
833
+ doc.moveDown(0.35);
834
+ drawSpaceBars(doc, ctx, deepDive.churn24h.bySpace);
835
+ drawTable(doc, ctx, {
836
+ title: 'Top Spaces by Offline Devices',
837
+ columns: [
838
+ { header: 'Space', width: 370, wrap: true },
839
+ { header: 'Offline', width: 90, align: 'right', wrap: false },
840
+ { header: 'Share', width: 90, align: 'right', wrap: false }
841
+ ],
842
+ rows: deepDive.topOfflineSpaces.map((row) => [row.space, String(row.offlineDevices), `${row.shareOfOfflinePct}%`]),
843
+ emptyMessage: 'No offline spaces found.'
844
+ });
845
+ drawTable(doc, ctx, {
846
+ title: 'Top Devices by Incident Volume',
847
+ columns: [
848
+ { header: 'Device', width: 370, wrap: true },
849
+ { header: 'Incidents', width: 90, align: 'right', wrap: false },
850
+ { header: 'Active', width: 90, align: 'right', wrap: false }
851
+ ],
852
+ rows: deepDive.topIncidentDevices.map((row) => [row.device, String(row.incidentCount), String(row.activeIncidents)]),
853
+ emptyMessage: 'No incident device concentration detected.'
854
+ });
855
+ drawTable(doc, ctx, {
856
+ title: 'Active Incident Aging',
857
+ columns: [
858
+ { header: 'Device', width: 120, wrap: true },
859
+ { header: 'Space', width: 230, wrap: true },
860
+ { header: 'Age (h)', width: 70, align: 'right', wrap: false },
861
+ { header: 'Created At', width: 130, wrap: false }
862
+ ],
863
+ rows: deepDive.activeIncidentAging.slice(0, 16).map((row) => [row.device, row.space, String(row.ageHours), formatUtcForReport(row.createdAtUtc)]),
864
+ emptyMessage: 'No active incidents.'
865
+ });
866
+ drawTable(doc, ctx, {
867
+ title: `${deepDive.windowHours}-Hour Churn by Space`,
868
+ columns: [
869
+ { header: 'Space', width: 450, wrap: true },
870
+ { header: 'Incidents', width: 100, align: 'right', wrap: false }
871
+ ],
872
+ rows: deepDive.churn24h.bySpace.map((row) => [row.space, String(row.incidents)]),
873
+ emptyMessage: 'No churn events in this window.'
874
+ });
875
+ drawTable(doc, ctx, {
876
+ title: 'Oldest Open Tickets',
877
+ columns: [
878
+ { header: 'Ticket', width: 88, wrap: false },
879
+ { header: 'Title', width: 182, wrap: true },
880
+ { header: 'Age (h)', width: 62, align: 'right', wrap: false },
881
+ { header: 'Device', width: 88, wrap: false },
882
+ { header: 'Created At', width: 130, wrap: false }
883
+ ],
884
+ rows: deepDive.ticketPosture.oldestOpenTickets.slice(0, 12).map((row) => [
885
+ redactSensitive(row.ticketId, includeSensitive),
886
+ row.title,
887
+ String(row.ageHours),
888
+ redactSensitive(row.deviceId, includeSensitive),
889
+ formatUtcForReport(row.createdAtUtc)
890
+ ]),
891
+ emptyMessage: 'No open tickets.'
892
+ });
893
+ if (deepDive.dataQuality.statusMismatches.length) {
894
+ drawTable(doc, ctx, {
895
+ title: 'Data Quality: Status Mismatches',
896
+ columns: [
897
+ { header: 'Device', width: 120, wrap: true },
898
+ { header: 'status', width: 70, wrap: false },
899
+ { header: 'state.status', width: 90, wrap: false },
900
+ { header: 'Last Seen', width: 130, wrap: false },
901
+ { header: 'Space', width: 160, wrap: true }
902
+ ],
903
+ rows: deepDive.dataQuality.statusMismatches.map((row) => [
904
+ row.device,
905
+ row.status,
906
+ row.stateStatus,
907
+ formatUtcForReport(row.lastSeen),
908
+ row.space
909
+ ])
910
+ });
911
+ }
912
+ const pages = doc.bufferedPageRange();
913
+ for (let index = pages.start; index < pages.start + pages.count; index += 1) {
914
+ doc.switchToPage(index);
915
+ drawPdfFooter(doc, ctx, index - pages.start + 1, pages.count);
916
+ }
917
+ doc.end();
918
+ });
919
+ }
920
+ async function generateFleetReport(args) {
921
+ const markdown = formatDeepDiveMarkdown(args.deepDive, args.includeSensitive);
922
+ ensureDir(args.outPath);
923
+ if (args.format === 'markdown') {
924
+ (0, node_fs_1.writeFileSync)(args.outPath, markdown, 'utf8');
925
+ }
926
+ else {
927
+ await renderBrandedPdfReport(args.deepDive, args.outPath, args.includeSensitive);
928
+ }
929
+ return {
930
+ schemaVersion: versions_1.REPORT_SCHEMA_VERSION,
931
+ generatedAtUtc: new Date().toISOString(),
932
+ tenantId: args.deepDive.tenantId,
933
+ format: args.format,
934
+ outputPath: (0, node_path_1.resolve)(args.outPath),
935
+ includeSensitive: args.includeSensitive
936
+ };
937
+ }
938
+ //# sourceMappingURL=fleet-insights.js.map