een-api-toolkit 0.3.43 → 0.3.47

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 (44) hide show
  1. package/.claude/agents/docs-accuracy-reviewer.md +35 -5
  2. package/.claude/agents/een-automations-agent.md +264 -0
  3. package/.claude/agents/een-devices-agent.md +5 -7
  4. package/.claude/agents/een-events-agent.md +30 -16
  5. package/.claude/agents/een-media-agent.md +12 -15
  6. package/.claude/agents/een-users-agent.md +2 -2
  7. package/CHANGELOG.md +6 -8
  8. package/dist/index.cjs +3 -3
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.ts +815 -0
  11. package/dist/index.js +986 -719
  12. package/dist/index.js.map +1 -1
  13. package/docs/AI-CONTEXT.md +17 -1
  14. package/docs/ai-reference/AI-AUTH.md +1 -1
  15. package/docs/ai-reference/AI-AUTOMATIONS.md +833 -0
  16. package/docs/ai-reference/AI-DEVICES.md +1 -1
  17. package/docs/ai-reference/AI-EVENTS.md +1 -1
  18. package/docs/ai-reference/AI-GROUPING.md +128 -66
  19. package/docs/ai-reference/AI-MEDIA.md +1 -1
  20. package/docs/ai-reference/AI-SETUP.md +1 -1
  21. package/docs/ai-reference/AI-USERS.md +1 -1
  22. package/examples/vue-automations/.env.example +11 -0
  23. package/examples/vue-automations/README.md +205 -0
  24. package/examples/vue-automations/e2e/app.spec.ts +83 -0
  25. package/examples/vue-automations/e2e/auth.spec.ts +468 -0
  26. package/examples/vue-automations/index.html +13 -0
  27. package/examples/vue-automations/package-lock.json +1722 -0
  28. package/examples/vue-automations/package.json +29 -0
  29. package/examples/vue-automations/playwright.config.ts +46 -0
  30. package/examples/vue-automations/src/App.vue +122 -0
  31. package/examples/vue-automations/src/main.ts +23 -0
  32. package/examples/vue-automations/src/router/index.ts +61 -0
  33. package/examples/vue-automations/src/views/Automations.vue +692 -0
  34. package/examples/vue-automations/src/views/Callback.vue +76 -0
  35. package/examples/vue-automations/src/views/Home.vue +172 -0
  36. package/examples/vue-automations/src/views/Login.vue +33 -0
  37. package/examples/vue-automations/src/views/Logout.vue +66 -0
  38. package/examples/vue-automations/src/vite-env.d.ts +1 -0
  39. package/examples/vue-automations/tsconfig.json +21 -0
  40. package/examples/vue-automations/tsconfig.node.json +10 -0
  41. package/examples/vue-automations/vite.config.ts +12 -0
  42. package/examples/vue-event-subscriptions/e2e/auth.spec.ts +8 -12
  43. package/package.json +1 -1
  44. package/scripts/setup-agents.ts +38 -19
@@ -0,0 +1,468 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+
3
+ /**
4
+ * E2E tests for the Vue Automations Example - OAuth Login Flow
5
+ *
6
+ * Tests the OAuth login flow through the UI:
7
+ * 1. Click login button in the example app
8
+ * 2. Enter credentials on EEN OAuth page
9
+ * 3. Complete the OAuth callback
10
+ * 4. Verify authenticated state and automations display
11
+ *
12
+ * Required environment variables:
13
+ * - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
14
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
15
+ * - TEST_USER: Test user email
16
+ * - TEST_PASSWORD: Test user password
17
+ */
18
+
19
+ const TIMEOUTS = {
20
+ OAUTH_REDIRECT: 30000,
21
+ ELEMENT_VISIBLE: 15000,
22
+ PASSWORD_VISIBLE: 10000,
23
+ AUTH_COMPLETE: 30000,
24
+ UI_UPDATE: 10000,
25
+ PROXY_CHECK: 5000,
26
+ DATA_LOAD: 20000
27
+ } as const
28
+
29
+ const TEST_USER = process.env.TEST_USER
30
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
31
+ const PROXY_URL = process.env.VITE_PROXY_URL
32
+
33
+ async function isProxyAccessible(): Promise<boolean> {
34
+ if (!PROXY_URL) return false
35
+ const controller = new AbortController()
36
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
37
+
38
+ try {
39
+ const response = await fetch(PROXY_URL, {
40
+ method: 'HEAD',
41
+ signal: controller.signal
42
+ })
43
+ return response.ok || response.status === 404
44
+ } catch {
45
+ return false
46
+ } finally {
47
+ clearTimeout(timeoutId)
48
+ }
49
+ }
50
+
51
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
52
+ await page.goto('/')
53
+
54
+ // Click login button on home page to go to login page
55
+ await page.click('[data-testid="login-button"]')
56
+ await page.waitForURL('/login')
57
+
58
+ // Click the OAuth login button on the login page
59
+ await Promise.all([
60
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
61
+ page.click('button:has-text("Login with Eagle Eye Networks")')
62
+ ])
63
+
64
+ const emailInput = page.locator('#authentication--input__email')
65
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
66
+ await emailInput.fill(username)
67
+
68
+ await page.getByRole('button', { name: 'Next' }).click()
69
+
70
+ const passwordInput = page.locator('#authentication--input__password')
71
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
72
+ await passwordInput.fill(password)
73
+
74
+ await page.locator('#next, button:has-text("Sign in")').first().click()
75
+
76
+ // After login, the automations app redirects to /automations
77
+ await page.waitForURL('**/automations', { timeout: TIMEOUTS.AUTH_COMPLETE })
78
+ }
79
+
80
+ async function clearAuthState(page: Page): Promise<void> {
81
+ try {
82
+ const url = page.url()
83
+ if (url && url.startsWith('http')) {
84
+ await page.evaluate(() => {
85
+ try {
86
+ localStorage.clear()
87
+ sessionStorage.clear()
88
+ } catch {
89
+ // Ignore
90
+ }
91
+ })
92
+ }
93
+ } catch {
94
+ // Ignore
95
+ }
96
+ }
97
+
98
+ test.describe('Vue Automations Example - Auth', () => {
99
+ let proxyAccessible = false
100
+
101
+ function skipIfNoProxy() {
102
+ test.skip(!proxyAccessible, 'OAuth proxy not accessible')
103
+ }
104
+
105
+ function skipIfNoCredentials() {
106
+ test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
107
+ }
108
+
109
+ test.beforeAll(async () => {
110
+ proxyAccessible = await isProxyAccessible()
111
+ if (!proxyAccessible) {
112
+ console.log('OAuth proxy not accessible - OAuth tests will be skipped')
113
+ }
114
+ })
115
+
116
+ test.afterEach(async ({ page }) => {
117
+ await clearAuthState(page)
118
+ })
119
+
120
+ test('shows login button when not authenticated', async ({ page }) => {
121
+ await page.goto('/')
122
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
123
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
124
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
125
+ await expect(page.locator('[data-testid="nav-automations"]')).not.toBeVisible()
126
+ })
127
+
128
+ test('automations page redirects to login when not authenticated', async ({ page }) => {
129
+ await page.goto('/automations')
130
+ // Should redirect to login page
131
+ await expect(page.locator('[data-testid="login-title"]')).toContainText('Login')
132
+ })
133
+
134
+ test('login button redirects to OAuth page', async ({ page }) => {
135
+ skipIfNoProxy()
136
+ skipIfNoCredentials()
137
+
138
+ await page.goto('/')
139
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
140
+
141
+ // Click login button to go to login page
142
+ await page.click('[data-testid="login-button"]')
143
+ await page.waitForURL('/login')
144
+
145
+ // Click the OAuth login button
146
+ await Promise.all([
147
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
148
+ page.click('button:has-text("Login with Eagle Eye Networks")')
149
+ ])
150
+
151
+ const emailInput = page.locator('#authentication--input__email')
152
+ await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
153
+ })
154
+
155
+ test('complete OAuth login flow and verify landing URL', async ({ page }) => {
156
+ skipIfNoProxy()
157
+ skipIfNoCredentials()
158
+
159
+ // Verify initially not authenticated
160
+ await page.goto('/')
161
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
162
+
163
+ // Perform login
164
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
165
+
166
+ // Verify landing URL is the automations page
167
+ await expect(page).toHaveURL('http://127.0.0.1:3333/automations')
168
+
169
+ // Verify authenticated state - we're on automations page so check nav elements
170
+ await expect(page.locator('[data-testid="nav-automations"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
171
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible()
172
+ await expect(page.locator('[data-testid="nav-login"]')).not.toBeVisible()
173
+ })
174
+
175
+ test('can view automations after login', async ({ page }) => {
176
+ skipIfNoProxy()
177
+ skipIfNoCredentials()
178
+
179
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
180
+ await expect(page.locator('[data-testid="nav-automations"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
181
+
182
+ // Verify we're on the automations page
183
+ await expect(page).toHaveURL('http://127.0.0.1:3333/automations')
184
+
185
+ // Wait for data to load - should see either a table or "no data" message
186
+ await expect(
187
+ page.locator('[data-testid="event-alert-rules-table"], .no-data, .loading')
188
+ ).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
189
+
190
+ // Should not see error state
191
+ await expect(page.locator('.error')).not.toBeVisible()
192
+ })
193
+
194
+ test('can switch between automation tabs', async ({ page }) => {
195
+ skipIfNoProxy()
196
+ skipIfNoCredentials()
197
+
198
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
199
+
200
+ // Default tab is Event Alert Rules
201
+ await expect(page.locator('[data-testid="tab-event-alert-rules"]')).toHaveClass(/active/)
202
+
203
+ // Click on Condition Rules tab
204
+ await page.click('[data-testid="tab-condition-rules"]')
205
+ await expect(page.locator('[data-testid="tab-condition-rules"]')).toHaveClass(/active/)
206
+ await expect(page.locator('[data-testid="condition-rules-content"]')).toBeVisible()
207
+
208
+ // Click on Action Rules tab
209
+ await page.click('[data-testid="tab-action-rules"]')
210
+ await expect(page.locator('[data-testid="tab-action-rules"]')).toHaveClass(/active/)
211
+ await expect(page.locator('[data-testid="action-rules-content"]')).toBeVisible()
212
+
213
+ // Click on Actions tab
214
+ await page.click('[data-testid="tab-actions"]')
215
+ await expect(page.locator('[data-testid="tab-actions"]')).toHaveClass(/active/)
216
+ await expect(page.locator('[data-testid="actions-content"]')).toBeVisible()
217
+ })
218
+
219
+ test('action rules tab loads data without errors', async ({ page }) => {
220
+ skipIfNoProxy()
221
+ skipIfNoCredentials()
222
+
223
+ // Collect console errors
224
+ const consoleErrors: string[] = []
225
+ page.on('console', msg => {
226
+ if (msg.type() === 'error') {
227
+ consoleErrors.push(msg.text())
228
+ }
229
+ })
230
+
231
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
232
+
233
+ // Click on Action Rules tab
234
+ await page.click('[data-testid="tab-action-rules"]')
235
+ await expect(page.locator('[data-testid="tab-action-rules"]')).toHaveClass(/active/)
236
+
237
+ // Wait for data to load - the TEST account should have action rules data
238
+ const table = page.locator('[data-testid="action-rules-table"]')
239
+ await expect(table).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
240
+
241
+ // Verify actual data rows exist (not just the header)
242
+ const rows = table.locator('tbody tr')
243
+ const rowCount = await rows.count()
244
+ expect(rowCount).toBeGreaterThan(0)
245
+
246
+ // Verify no error state is shown
247
+ await expect(page.locator('[data-testid="action-rules-content"] .error')).not.toBeVisible()
248
+
249
+ // Check for JavaScript errors during render
250
+ const renderErrors = consoleErrors.filter(e =>
251
+ e.includes('TypeError') || e.includes('Cannot read properties')
252
+ )
253
+ expect(renderErrors).toHaveLength(0)
254
+ })
255
+
256
+ test('condition rules tab loads data without errors', async ({ page }) => {
257
+ skipIfNoProxy()
258
+ skipIfNoCredentials()
259
+
260
+ // Collect console errors
261
+ const consoleErrors: string[] = []
262
+ page.on('console', msg => {
263
+ if (msg.type() === 'error') {
264
+ consoleErrors.push(msg.text())
265
+ }
266
+ })
267
+
268
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
269
+
270
+ // Click on Condition Rules tab
271
+ await page.click('[data-testid="tab-condition-rules"]')
272
+ await expect(page.locator('[data-testid="tab-condition-rules"]')).toHaveClass(/active/)
273
+
274
+ // Wait for data to load - the TEST account should have condition rules data
275
+ const table = page.locator('[data-testid="condition-rules-table"]')
276
+ await expect(table).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
277
+
278
+ // Verify actual data rows exist (not just the header)
279
+ const rows = table.locator('tbody tr')
280
+ const rowCount = await rows.count()
281
+ expect(rowCount).toBeGreaterThan(0)
282
+
283
+ // Verify no error state is shown
284
+ await expect(page.locator('[data-testid="condition-rules-content"] .error')).not.toBeVisible()
285
+
286
+ // Check for JavaScript errors during render
287
+ const renderErrors = consoleErrors.filter(e =>
288
+ e.includes('TypeError') || e.includes('Cannot read properties')
289
+ )
290
+ expect(renderErrors).toHaveLength(0)
291
+ })
292
+
293
+ test('actions tab loads data without errors', async ({ page }) => {
294
+ skipIfNoProxy()
295
+ skipIfNoCredentials()
296
+
297
+ // Collect console errors
298
+ const consoleErrors: string[] = []
299
+ page.on('console', msg => {
300
+ if (msg.type() === 'error') {
301
+ consoleErrors.push(msg.text())
302
+ }
303
+ })
304
+
305
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
306
+
307
+ // Click on Actions tab
308
+ await page.click('[data-testid="tab-actions"]')
309
+ await expect(page.locator('[data-testid="tab-actions"]')).toHaveClass(/active/)
310
+
311
+ // Wait for data to load - the TEST account should have actions data
312
+ const table = page.locator('[data-testid="actions-table"]')
313
+ await expect(table).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
314
+
315
+ // Verify actual data rows exist (not just the header)
316
+ const rows = table.locator('tbody tr')
317
+ const rowCount = await rows.count()
318
+ expect(rowCount).toBeGreaterThan(0)
319
+
320
+ // Verify no error state is shown
321
+ await expect(page.locator('[data-testid="actions-content"] .error')).not.toBeVisible()
322
+
323
+ // Check for JavaScript errors during render
324
+ const renderErrors = consoleErrors.filter(e =>
325
+ e.includes('TypeError') || e.includes('Cannot read properties')
326
+ )
327
+ expect(renderErrors).toHaveLength(0)
328
+ })
329
+
330
+ test('event alert rules tab loads data without errors', async ({ page }) => {
331
+ skipIfNoProxy()
332
+ skipIfNoCredentials()
333
+
334
+ // Collect console errors
335
+ const consoleErrors: string[] = []
336
+ page.on('console', msg => {
337
+ if (msg.type() === 'error') {
338
+ consoleErrors.push(msg.text())
339
+ }
340
+ })
341
+
342
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
343
+
344
+ // Event alert rules is the default tab - the TEST account should have data
345
+ const table = page.locator('[data-testid="event-alert-rules-table"]')
346
+ await expect(table).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
347
+
348
+ // Verify actual data rows exist (not just the header)
349
+ const rows = table.locator('tbody tr')
350
+ const rowCount = await rows.count()
351
+ expect(rowCount).toBeGreaterThan(0)
352
+
353
+ // Verify no error state is shown
354
+ await expect(page.locator('[data-testid="event-alert-rules-content"] .error')).not.toBeVisible()
355
+
356
+ // Check for JavaScript errors during render
357
+ const renderErrors = consoleErrors.filter(e =>
358
+ e.includes('TypeError') || e.includes('Cannot read properties')
359
+ )
360
+ expect(renderErrors).toHaveLength(0)
361
+ })
362
+
363
+ test('clicking a row opens modal with JSON data', async ({ page }) => {
364
+ skipIfNoProxy()
365
+ skipIfNoCredentials()
366
+
367
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
368
+
369
+ // Wait for event alert rules to load (default tab)
370
+ const table = page.locator('[data-testid="event-alert-rules-table"]')
371
+ await expect(table).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
372
+
373
+ // Click the first row
374
+ const firstRow = table.locator('tbody tr').first()
375
+ await firstRow.click()
376
+
377
+ // Verify modal appears
378
+ const modal = page.locator('[data-testid="detail-modal"]')
379
+ await expect(modal).toBeVisible()
380
+
381
+ // Verify modal has JSON content
382
+ const jsonContent = page.locator('[data-testid="modal-json"]')
383
+ await expect(jsonContent).toBeVisible()
384
+ const jsonText = await jsonContent.textContent()
385
+ expect(jsonText).toContain('"id"')
386
+ expect(jsonText).toContain('"name"')
387
+
388
+ // Close modal by clicking close button
389
+ await page.click('[data-testid="modal-close"]')
390
+ await expect(modal).not.toBeVisible()
391
+ })
392
+
393
+ test('modal can be closed by clicking overlay', async ({ page }) => {
394
+ skipIfNoProxy()
395
+ skipIfNoCredentials()
396
+
397
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
398
+
399
+ // Wait for event alert rules to load
400
+ const table = page.locator('[data-testid="event-alert-rules-table"]')
401
+ await expect(table).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
402
+
403
+ // Click the first row to open modal
404
+ await table.locator('tbody tr').first().click()
405
+
406
+ // Verify modal appears
407
+ const modal = page.locator('[data-testid="detail-modal"]')
408
+ await expect(modal).toBeVisible()
409
+
410
+ // Close modal by clicking overlay (outside the modal)
411
+ await page.click('[data-testid="modal-overlay"]', { position: { x: 10, y: 10 } })
412
+ await expect(modal).not.toBeVisible()
413
+ })
414
+
415
+ test('can use enabled filter', async ({ page }) => {
416
+ skipIfNoProxy()
417
+ skipIfNoCredentials()
418
+
419
+ // Go to home and check if already logged in
420
+ await page.goto('/')
421
+ await page.waitForTimeout(1000)
422
+
423
+ const isLoggedIn = await page.locator('[data-testid="nav-logout"]').isVisible()
424
+
425
+ if (!isLoggedIn) {
426
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
427
+ } else {
428
+ // Navigate to automations if already logged in
429
+ await page.click('[data-testid="nav-automations"]')
430
+ await page.waitForURL('**/automations')
431
+ }
432
+
433
+ // Wait for initial data load
434
+ await expect(
435
+ page.locator('[data-testid="event-alert-rules-table"], .no-data')
436
+ ).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
437
+
438
+ // Change filter to enabled only
439
+ await page.selectOption('[data-testid="enabled-filter"]', 'enabled')
440
+
441
+ // Wait for data to reload
442
+ await page.waitForTimeout(1000)
443
+
444
+ // Should still show content (table or no-data) without error
445
+ await expect(page.locator('.error')).not.toBeVisible()
446
+ })
447
+
448
+ test('can logout after login', async ({ page }) => {
449
+ skipIfNoProxy()
450
+ skipIfNoCredentials()
451
+
452
+ // Always perform login to ensure test isolation
453
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
454
+
455
+ // Go to home to see the logout button
456
+ await page.goto('/')
457
+
458
+ const logoutButton = page.locator('[data-testid="nav-logout"]')
459
+ await expect(logoutButton).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
460
+
461
+ await logoutButton.click()
462
+
463
+ // Wait for logout to complete and redirect to home
464
+ await page.waitForURL('**/', { timeout: TIMEOUTS.AUTH_COMPLETE })
465
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
466
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
467
+ })
468
+ })
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>EEN Automations Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>