een-api-toolkit 0.3.28 → 0.3.35
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.
- package/.claude/agents/docs-accuracy-reviewer.md +15 -3
- package/.claude/agents/een-auth-agent.md +131 -0
- package/.claude/agents/een-devices-agent.md +48 -13
- package/.claude/agents/een-events-agent.md +98 -0
- package/.claude/agents/een-grouping-agent.md +394 -0
- package/.claude/agents/een-media-agent.md +25 -5
- package/CHANGELOG.md +99 -10
- package/README.md +5 -3
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +388 -218
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +13 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +411 -0
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-alerts-metrics/README.md +2 -0
- package/examples/vue-alerts-metrics/alert-metrics-screenshot.png +0 -0
- package/examples/vue-alerts-metrics/e2e/auth.spec.ts +1 -1
- package/examples/vue-alerts-metrics/package-lock.json +17 -14
- package/examples/vue-alerts-metrics/package.json +1 -1
- package/examples/vue-bridges/package-lock.json +21 -15
- package/examples/vue-bridges/package.json +1 -1
- package/examples/vue-cameras/package-lock.json +21 -15
- package/examples/vue-cameras/package.json +1 -1
- package/examples/vue-event-subscriptions/README.md +2 -0
- package/examples/vue-event-subscriptions/event-subscriptions-screenshot.png +0 -0
- package/examples/vue-event-subscriptions/package-lock.json +17 -14
- package/examples/vue-event-subscriptions/package.json +1 -1
- package/examples/vue-events/events-screenshot.png +0 -0
- package/examples/vue-events/package-lock.json +17 -14
- package/examples/vue-events/package.json +1 -1
- package/examples/vue-feeds/package-lock.json +21 -15
- package/examples/vue-feeds/package.json +1 -1
- package/examples/vue-layouts/.env.example +12 -0
- package/examples/vue-layouts/README.md +320 -0
- package/examples/vue-layouts/e2e/app.spec.ts +76 -0
- package/examples/vue-layouts/e2e/auth.spec.ts +264 -0
- package/examples/vue-layouts/index.html +13 -0
- package/examples/vue-layouts/layouts-screenshot.png +0 -0
- package/examples/vue-layouts/package-lock.json +1722 -0
- package/examples/vue-layouts/package.json +28 -0
- package/examples/vue-layouts/playwright.config.ts +47 -0
- package/examples/vue-layouts/src/App.vue +124 -0
- package/examples/vue-layouts/src/components/LayoutModal.vue +456 -0
- package/examples/vue-layouts/src/main.ts +25 -0
- package/examples/vue-layouts/src/router/index.ts +62 -0
- package/examples/vue-layouts/src/views/Callback.vue +76 -0
- package/examples/vue-layouts/src/views/Home.vue +188 -0
- package/examples/vue-layouts/src/views/Layouts.vue +355 -0
- package/examples/vue-layouts/src/views/Login.vue +33 -0
- package/examples/vue-layouts/src/views/Logout.vue +59 -0
- package/examples/vue-layouts/src/vite-env.d.ts +12 -0
- package/examples/vue-layouts/tsconfig.json +21 -0
- package/examples/vue-layouts/tsconfig.node.json +10 -0
- package/examples/vue-layouts/vite.config.ts +12 -0
- package/examples/vue-media/media-screenshot.png +0 -0
- package/examples/vue-media/package-lock.json +19 -14
- package/examples/vue-media/package.json +1 -1
- package/examples/vue-users/package-lock.json +21 -16
- package/examples/vue-users/package.json +2 -2
- package/package.json +2 -2
- package/scripts/setup-agents.ts +0 -0
|
@@ -61,6 +61,8 @@ assistant: "I'll use the docs-accuracy-reviewer agent to verify the README and a
|
|
|
61
61
|
|
|
62
62
|
1. **Discovery Phase**:
|
|
63
63
|
- List all markdown files in the project (README.md, docs/**, CLAUDE.md, etc.)
|
|
64
|
+
- **Scan ALL example directories** (`examples/*/README.md`) - do not skip any
|
|
65
|
+
- Check agent files in `.claude/agents/*.md`
|
|
64
66
|
- Identify the source code structure for cross-referencing
|
|
65
67
|
|
|
66
68
|
2. **Analysis Phase**:
|
|
@@ -82,6 +84,15 @@ assistant: "I'll use the docs-accuracy-reviewer agent to verify the README and a
|
|
|
82
84
|
|
|
83
85
|
## Specific Checks to Perform
|
|
84
86
|
|
|
87
|
+
### For Example Application Documentation:
|
|
88
|
+
- **Check ALL example apps** in `examples/*/README.md` (not just one)
|
|
89
|
+
- Verify screenshot references exist and filenames match actual files
|
|
90
|
+
- Confirm all listed API functions are exported from `src/index.ts`
|
|
91
|
+
- Check that `.env.example` files exist when referenced in setup instructions
|
|
92
|
+
- Validate project structure sections match actual directory contents
|
|
93
|
+
- Ensure port numbers are correct (should be `127.0.0.1:3333`)
|
|
94
|
+
- Verify code examples use current API signatures
|
|
95
|
+
|
|
85
96
|
### For API Documentation:
|
|
86
97
|
- Compare documented function signatures with `src/index.ts` exports
|
|
87
98
|
- Verify type definitions match `src/types/` directory
|
|
@@ -141,6 +152,7 @@ When reporting findings, use this structure:
|
|
|
141
152
|
|
|
142
153
|
Before completing your review:
|
|
143
154
|
1. Verify you've checked ALL markdown files in the project
|
|
144
|
-
2. Confirm
|
|
145
|
-
3.
|
|
146
|
-
4.
|
|
155
|
+
2. **Confirm ALL example app READMEs were reviewed** (list them in your report)
|
|
156
|
+
3. Confirm each fix you made is backed by evidence from source code
|
|
157
|
+
4. Re-read modified sections to ensure they're clear and accurate
|
|
158
|
+
5. Check that your fixes didn't introduce new broken links or inconsistencies
|
|
@@ -123,10 +123,22 @@ authStore.isExpired // Computed: true if token expired
|
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
## Auth Guard Pattern
|
|
126
|
+
|
|
127
|
+
**CRITICAL**: The OAuth callback check MUST come BEFORE the auth check in the global guard.
|
|
128
|
+
The EEN IDP redirects to the root path (`/`) with `code` and `state` query parameters.
|
|
129
|
+
If you check authentication first, the user will be redirected to login before the callback is processed.
|
|
130
|
+
|
|
126
131
|
```typescript
|
|
127
132
|
import { useAuthStore } from 'een-api-toolkit'
|
|
128
133
|
|
|
129
134
|
router.beforeEach((to, from, next) => {
|
|
135
|
+
// IMPORTANT: Check for OAuth callback FIRST, before auth check
|
|
136
|
+
// EEN IDP redirects to root path with code and state params
|
|
137
|
+
if (to.path === '/' && to.query.code && to.query.state) {
|
|
138
|
+
next({ name: 'callback', query: to.query })
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
const authStore = useAuthStore()
|
|
131
143
|
|
|
132
144
|
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
@@ -137,6 +149,10 @@ router.beforeEach((to, from, next) => {
|
|
|
137
149
|
})
|
|
138
150
|
```
|
|
139
151
|
|
|
152
|
+
**WARNING**: Do NOT use route-specific `beforeEnter` guards for OAuth callback detection.
|
|
153
|
+
Global `beforeEach` guards run BEFORE route-specific guards, so the auth check will
|
|
154
|
+
block the callback before `beforeEnter` can redirect to the callback handler.
|
|
155
|
+
|
|
140
156
|
## Token Lifecycle
|
|
141
157
|
|
|
142
158
|
1. **Login**: User redirects to EEN OAuth → Returns with code → Exchange for tokens
|
|
@@ -151,6 +167,38 @@ router.beforeEach((to, from, next) => {
|
|
|
151
167
|
- **Session ID**: Client receives session ID to identify refresh session
|
|
152
168
|
- **Token only**: Client stores only short-lived access token
|
|
153
169
|
|
|
170
|
+
## Environment Variables
|
|
171
|
+
|
|
172
|
+
Required environment variables for OAuth:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
VITE_PROXY_URL=https://your-oauth-proxy.workers.dev # OAuth proxy URL
|
|
176
|
+
VITE_EEN_CLIENT_ID=YOUR-CLIENT-ID # EEN OAuth client ID
|
|
177
|
+
TEST_USER=user@example.com # For Playwright tests
|
|
178
|
+
TEST_PASSWORD=password # For Playwright tests
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## localStorage Keys
|
|
182
|
+
|
|
183
|
+
The toolkit stores auth state in localStorage with these keys:
|
|
184
|
+
|
|
185
|
+
| Key | Description |
|
|
186
|
+
|-----|-------------|
|
|
187
|
+
| `een_token` | JWT access token |
|
|
188
|
+
| `een_tokenExpiration` | Token expiration timestamp (ms) |
|
|
189
|
+
| `een_sessionId` | Session ID for token refresh (proxy-side) |
|
|
190
|
+
| `een_hostname` | EEN API hostname (region-specific, e.g., `api.c021.eagleeyenetworks.com`) |
|
|
191
|
+
| `een_userProfile` | Cached user profile JSON |
|
|
192
|
+
| `een_refreshTokenMarker` | Indicates refresh token exists server-side (`"present"`) |
|
|
193
|
+
|
|
194
|
+
Useful for debugging:
|
|
195
|
+
```typescript
|
|
196
|
+
// Check auth state in browser console
|
|
197
|
+
console.log('Token:', localStorage.getItem('een_token')?.substring(0, 50) + '...')
|
|
198
|
+
console.log('Expires:', new Date(parseInt(localStorage.getItem('een_tokenExpiration') || '0')))
|
|
199
|
+
console.log('Hostname:', localStorage.getItem('een_hostname'))
|
|
200
|
+
```
|
|
201
|
+
|
|
154
202
|
## Constraints
|
|
155
203
|
- Never expose refresh tokens to client code
|
|
156
204
|
- Handle AUTH_REQUIRED errors by redirecting to login
|
|
@@ -158,6 +206,89 @@ router.beforeEach((to, from, next) => {
|
|
|
158
206
|
- Always validate state parameter in callback
|
|
159
207
|
- Clear auth state completely on logout
|
|
160
208
|
|
|
209
|
+
## Vite Server Configuration
|
|
210
|
+
|
|
211
|
+
The Vite dev server MUST bind to `127.0.0.1` (not `localhost`) to match the redirect URI:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// vite.config.ts
|
|
215
|
+
export default defineConfig({
|
|
216
|
+
server: {
|
|
217
|
+
host: '127.0.0.1', // REQUIRED: Must match redirect URI
|
|
218
|
+
port: 3333,
|
|
219
|
+
strictPort: true
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## EEN Login Page (Two-Step Process)
|
|
225
|
+
|
|
226
|
+
The EEN OAuth login page uses a **two-step authentication flow**:
|
|
227
|
+
|
|
228
|
+
1. **Step 1 - Email**: User enters email address and clicks "Next"
|
|
229
|
+
2. **Step 2 - Password**: Password field appears, user enters password and clicks "Sign in"
|
|
230
|
+
|
|
231
|
+
This is important for Playwright tests - you cannot fill both fields at once:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Playwright test example for EEN two-step login
|
|
235
|
+
// Step 1: Enter email and click Next
|
|
236
|
+
const emailInput = page.locator('input[type="email"], input[type="text"]').first()
|
|
237
|
+
await emailInput.fill(TEST_USER)
|
|
238
|
+
await page.getByRole('button', { name: /next/i }).click()
|
|
239
|
+
|
|
240
|
+
// Step 2: Wait for password field and fill it
|
|
241
|
+
const passwordInput = page.locator('input[type="password"]')
|
|
242
|
+
await passwordInput.waitFor({ state: 'visible', timeout: 10000 })
|
|
243
|
+
await passwordInput.fill(TEST_PASSWORD)
|
|
244
|
+
await page.getByRole('button', { name: /sign in/i }).click()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Playwright E2E Test Patterns
|
|
248
|
+
|
|
249
|
+
**Reference examples in:** `node_modules/een-api-toolkit/examples/*/e2e/auth.spec.ts`
|
|
250
|
+
|
|
251
|
+
Best practices for auth testing:
|
|
252
|
+
1. **Fresh login per test**: Perform login for each test that needs auth (don't rely on state persistence)
|
|
253
|
+
2. **Clear state after each test**: Use `afterEach` to clear localStorage/sessionStorage
|
|
254
|
+
3. **Check proxy accessibility**: Skip OAuth tests if proxy is not reachable
|
|
255
|
+
4. **Use EEN-specific selectors**: The EEN login page has specific IDs like `#authentication--input__email`
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Complete performLogin helper function
|
|
259
|
+
async function performLogin(page: Page, username: string, password: string): Promise<void> {
|
|
260
|
+
await page.goto('/login')
|
|
261
|
+
await page.click('button:has-text("Login with Eagle Eye Networks")')
|
|
262
|
+
|
|
263
|
+
// Wait for EEN OAuth page
|
|
264
|
+
await page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: 30000 })
|
|
265
|
+
|
|
266
|
+
// Step 1: Email
|
|
267
|
+
const emailInput = page.locator('#authentication--input__email, input[type="email"]').first()
|
|
268
|
+
await emailInput.waitFor({ state: 'visible', timeout: 15000 })
|
|
269
|
+
await emailInput.fill(username)
|
|
270
|
+
await page.getByRole('button', { name: 'Next' }).click()
|
|
271
|
+
|
|
272
|
+
// Step 2: Password
|
|
273
|
+
const passwordInput = page.locator('#authentication--input__password, input[type="password"]')
|
|
274
|
+
await passwordInput.waitFor({ state: 'visible', timeout: 10000 })
|
|
275
|
+
await passwordInput.fill(password)
|
|
276
|
+
await page.locator('#next, button:has-text("Sign in")').first().click()
|
|
277
|
+
|
|
278
|
+
// Wait for redirect back to app
|
|
279
|
+
await page.waitForURL(/127\.0\.0\.1:3333/, { timeout: 60000 })
|
|
280
|
+
await page.waitForURL('**/', { timeout: 60000 })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Clear auth state helper
|
|
284
|
+
async function clearAuthState(page: Page): Promise<void> {
|
|
285
|
+
await page.evaluate(() => {
|
|
286
|
+
localStorage.clear()
|
|
287
|
+
sessionStorage.clear()
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
161
292
|
## Common Errors
|
|
162
293
|
|
|
163
294
|
| Error | Cause | Solution |
|
|
@@ -69,14 +69,14 @@ interface Camera {
|
|
|
69
69
|
type CameraStatus =
|
|
70
70
|
| 'online'
|
|
71
71
|
| 'offline'
|
|
72
|
-
| 'streaming'
|
|
73
|
-
| 'recording'
|
|
74
|
-
| 'registered'
|
|
75
72
|
| 'deviceOffline'
|
|
76
73
|
| 'bridgeOffline'
|
|
77
74
|
| 'invalidCredentials'
|
|
78
75
|
| 'error'
|
|
79
|
-
| '
|
|
76
|
+
| 'streaming'
|
|
77
|
+
| 'registered'
|
|
78
|
+
| 'attaching'
|
|
79
|
+
| 'initializing'
|
|
80
80
|
|
|
81
81
|
// Status can also be nested in an object:
|
|
82
82
|
// camera.status?.connectionStatus
|
|
@@ -97,7 +97,10 @@ type BridgeStatus =
|
|
|
97
97
|
| 'online'
|
|
98
98
|
| 'offline'
|
|
99
99
|
| 'error'
|
|
100
|
-
| '
|
|
100
|
+
| 'idle'
|
|
101
|
+
| 'registered'
|
|
102
|
+
| 'attaching'
|
|
103
|
+
| 'initializing'
|
|
101
104
|
```
|
|
102
105
|
|
|
103
106
|
### ListCamerasParams
|
|
@@ -119,16 +122,29 @@ interface ListCamerasParams {
|
|
|
119
122
|
## Key Functions
|
|
120
123
|
|
|
121
124
|
### getCameras()
|
|
122
|
-
List cameras with optional filters
|
|
125
|
+
List cameras with optional filters.
|
|
126
|
+
|
|
127
|
+
**IMPORTANT:** The `status` field is NOT included by default. You must use `include: ['status']` to receive it:
|
|
128
|
+
|
|
123
129
|
```typescript
|
|
124
130
|
import { getCameras, type Camera, type ListCamerasParams } from 'een-api-toolkit'
|
|
125
131
|
|
|
126
132
|
const cameras = ref<Camera[]>([])
|
|
127
133
|
|
|
128
|
-
// Get all
|
|
134
|
+
// Get all cameras WITH status - include: ['status'] is required!
|
|
135
|
+
async function fetchCameras() {
|
|
136
|
+
const result = await getCameras({
|
|
137
|
+
include: ['status'], // Required to get camera.status
|
|
138
|
+
pageSize: 100
|
|
139
|
+
})
|
|
140
|
+
// Now camera.status will be populated
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get all online cameras (still need include for display)
|
|
129
144
|
async function fetchOnlineCameras() {
|
|
130
145
|
const result = await getCameras({
|
|
131
|
-
|
|
146
|
+
include: ['status'], // Required to display status in UI
|
|
147
|
+
status__in: ['online', 'streaming', 'registered'],
|
|
132
148
|
pageSize: 100
|
|
133
149
|
})
|
|
134
150
|
|
|
@@ -231,17 +247,33 @@ async function fetchBridge(bridgeId: string) {
|
|
|
231
247
|
|
|
232
248
|
```vue
|
|
233
249
|
<script setup lang="ts">
|
|
234
|
-
import { ref, onMounted } from 'vue'
|
|
235
|
-
import { getCameras, type Camera, type ListCamerasParams } from 'een-api-toolkit'
|
|
250
|
+
import { ref, onMounted, computed } from 'vue'
|
|
251
|
+
import { getCameras, type Camera, type CameraStatus, type ListCamerasParams } from 'een-api-toolkit'
|
|
236
252
|
|
|
237
253
|
const cameras = ref<Camera[]>([])
|
|
238
254
|
const loading = ref(false)
|
|
239
|
-
const statusFilter = ref<string[]>(['online', 'streaming', '
|
|
255
|
+
const statusFilter = ref<string[]>(['online', 'streaming', 'registered'])
|
|
256
|
+
|
|
257
|
+
// Helper: status can be a string OR an object with connectionStatus
|
|
258
|
+
function getStatusString(status?: CameraStatus | { connectionStatus?: CameraStatus }): string | undefined {
|
|
259
|
+
if (!status) return undefined
|
|
260
|
+
if (typeof status === 'string') return status
|
|
261
|
+
return status.connectionStatus
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Computed property to pre-process cameras with status string (avoids calling helper multiple times in template)
|
|
265
|
+
const camerasWithStatus = computed(() =>
|
|
266
|
+
cameras.value.map(camera => ({
|
|
267
|
+
...camera,
|
|
268
|
+
statusString: getStatusString(camera.status),
|
|
269
|
+
}))
|
|
270
|
+
)
|
|
240
271
|
|
|
241
272
|
async function fetchCameras() {
|
|
242
273
|
loading.value = true
|
|
243
274
|
|
|
244
275
|
const params: ListCamerasParams = {
|
|
276
|
+
include: ['status'], // Required to receive status field
|
|
245
277
|
pageSize: 100
|
|
246
278
|
}
|
|
247
279
|
|
|
@@ -275,10 +307,13 @@ onMounted(fetchCameras)
|
|
|
275
307
|
|
|
276
308
|
<div v-if="loading">Loading cameras...</div>
|
|
277
309
|
|
|
310
|
+
<!-- Use computed property for better performance (status string computed once per camera) -->
|
|
278
311
|
<div class="camera-grid" v-else>
|
|
279
|
-
<div v-for="camera in
|
|
312
|
+
<div v-for="camera in camerasWithStatus" :key="camera.id" class="camera-card">
|
|
280
313
|
<h3>{{ camera.name }}</h3>
|
|
281
|
-
<span :class="camera.
|
|
314
|
+
<span :class="camera.statusString">
|
|
315
|
+
{{ camera.statusString || 'unknown' }}
|
|
316
|
+
</span>
|
|
282
317
|
</div>
|
|
283
318
|
</div>
|
|
284
319
|
</div>
|
|
@@ -141,6 +141,77 @@ const actor = `camera:${cameraId}`
|
|
|
141
141
|
const actor = `account:${accountId}`
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### listEventFieldValues()
|
|
145
|
+
Discover available event types for a specific camera:
|
|
146
|
+
```typescript
|
|
147
|
+
import { listEventFieldValues } from 'een-api-toolkit'
|
|
148
|
+
|
|
149
|
+
async function getAvailableEventTypes(cameraId: string) {
|
|
150
|
+
const result = await listEventFieldValues({
|
|
151
|
+
actor: `camera:${cameraId}`
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (result.data) {
|
|
155
|
+
// result.data.type is an array of event type strings available for this camera
|
|
156
|
+
const availableTypes = result.data.type || []
|
|
157
|
+
// e.g., ['een.motionDetectionEvent.v1', 'een.tamperDetectionEvent.v1']
|
|
158
|
+
return availableTypes
|
|
159
|
+
}
|
|
160
|
+
return []
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### listEventTypes()
|
|
165
|
+
Get human-readable names for event types:
|
|
166
|
+
```typescript
|
|
167
|
+
import { listEventTypes } from 'een-api-toolkit'
|
|
168
|
+
|
|
169
|
+
async function fetchEventTypeNames() {
|
|
170
|
+
const result = await listEventTypes({ pageSize: 100 })
|
|
171
|
+
|
|
172
|
+
if (result.data) {
|
|
173
|
+
// Build a map of type -> name for display
|
|
174
|
+
const nameMap = new Map<string, string>()
|
|
175
|
+
for (const et of result.data.results) {
|
|
176
|
+
nameMap.set(et.type, et.name)
|
|
177
|
+
// e.g., 'een.motionDetectionEvent.v1' -> 'Motion Detection'
|
|
178
|
+
}
|
|
179
|
+
return nameMap
|
|
180
|
+
}
|
|
181
|
+
return new Map()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fallback: Parse event type string if API name not available
|
|
185
|
+
function parseEventTypeName(type: string): string {
|
|
186
|
+
const match = type.match(/een\.(\w+)Event\.v\d+/)
|
|
187
|
+
if (match) {
|
|
188
|
+
return match[1]
|
|
189
|
+
.replace(/([A-Z])/g, ' $1') // Add space before capitals
|
|
190
|
+
.replace(/^./, str => str.toUpperCase())
|
|
191
|
+
.trim()
|
|
192
|
+
}
|
|
193
|
+
return type
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Motion Detection Preselection Pattern
|
|
198
|
+
When implementing event type toggles, preselect motion detection by default:
|
|
199
|
+
```typescript
|
|
200
|
+
const MOTION_DETECTION_EVENT = 'een.motionDetectionEvent.v1'
|
|
201
|
+
|
|
202
|
+
function preselectEventTypes(availableTypes: string[]): string[] {
|
|
203
|
+
// Preselect motion detection if available
|
|
204
|
+
if (availableTypes.includes(MOTION_DETECTION_EVENT)) {
|
|
205
|
+
return [MOTION_DETECTION_EVENT]
|
|
206
|
+
}
|
|
207
|
+
// Otherwise select the first available type
|
|
208
|
+
if (availableTypes.length > 0) {
|
|
209
|
+
return [availableTypes[0]]
|
|
210
|
+
}
|
|
211
|
+
return []
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
144
215
|
### getEventMetrics()
|
|
145
216
|
Get aggregated event counts:
|
|
146
217
|
```typescript
|
|
@@ -262,6 +333,33 @@ onUnmounted(async () => {
|
|
|
262
333
|
})
|
|
263
334
|
```
|
|
264
335
|
|
|
336
|
+
## Getting Event Thumbnails
|
|
337
|
+
|
|
338
|
+
Use `getRecordedImage()` to fetch a thumbnail image for an event:
|
|
339
|
+
```typescript
|
|
340
|
+
import { getRecordedImage, type Event } from 'een-api-toolkit'
|
|
341
|
+
|
|
342
|
+
const eventImages = ref<Map<string, string>>(new Map())
|
|
343
|
+
|
|
344
|
+
async function fetchEventThumbnail(event: Event) {
|
|
345
|
+
// Extract camera ID from actor (format: "camera:{cameraId}")
|
|
346
|
+
const cameraId = event.actor.replace('camera:', '')
|
|
347
|
+
|
|
348
|
+
const result = await getRecordedImage({
|
|
349
|
+
cameraId,
|
|
350
|
+
timestamp: event.timestamp,
|
|
351
|
+
width: 120, // Thumbnail size
|
|
352
|
+
height: 80
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
if (result.data?.dataUrl) {
|
|
356
|
+
eventImages.value.set(event.id, result.data.dataUrl)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// In template: <img :src="eventImages.get(event.id)" />
|
|
361
|
+
```
|
|
362
|
+
|
|
265
363
|
## Displaying Event Bounding Boxes
|
|
266
364
|
|
|
267
365
|
Events can include SVG overlays showing where motion/objects were detected.
|