een-api-toolkit 0.3.47 → 0.3.49

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 (42) hide show
  1. package/.claude/agents/een-jobs-agent.md +676 -0
  2. package/CHANGELOG.md +7 -8
  3. package/dist/index.cjs +3 -3
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +1172 -28
  6. package/dist/index.js +796 -333
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +22 -1
  9. package/docs/ai-reference/AI-AUTH.md +1 -1
  10. package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
  11. package/docs/ai-reference/AI-DEVICES.md +1 -1
  12. package/docs/ai-reference/AI-EVENTS.md +1 -1
  13. package/docs/ai-reference/AI-GROUPING.md +1 -1
  14. package/docs/ai-reference/AI-JOBS.md +1084 -0
  15. package/docs/ai-reference/AI-MEDIA.md +1 -1
  16. package/docs/ai-reference/AI-SETUP.md +1 -1
  17. package/docs/ai-reference/AI-USERS.md +1 -1
  18. package/examples/vue-jobs/.env.example +11 -0
  19. package/examples/vue-jobs/README.md +245 -0
  20. package/examples/vue-jobs/e2e/app.spec.ts +79 -0
  21. package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
  22. package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
  23. package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
  24. package/examples/vue-jobs/index.html +13 -0
  25. package/examples/vue-jobs/package-lock.json +1722 -0
  26. package/examples/vue-jobs/package.json +28 -0
  27. package/examples/vue-jobs/playwright.config.ts +47 -0
  28. package/examples/vue-jobs/src/App.vue +154 -0
  29. package/examples/vue-jobs/src/main.ts +25 -0
  30. package/examples/vue-jobs/src/router/index.ts +82 -0
  31. package/examples/vue-jobs/src/views/Callback.vue +76 -0
  32. package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
  33. package/examples/vue-jobs/src/views/Files.vue +424 -0
  34. package/examples/vue-jobs/src/views/Home.vue +195 -0
  35. package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
  36. package/examples/vue-jobs/src/views/Jobs.vue +297 -0
  37. package/examples/vue-jobs/src/views/Login.vue +33 -0
  38. package/examples/vue-jobs/src/views/Logout.vue +59 -0
  39. package/examples/vue-jobs/src/vite-env.d.ts +1 -0
  40. package/examples/vue-jobs/tsconfig.json +25 -0
  41. package/examples/vue-jobs/vite.config.ts +12 -0
  42. package/package.json +1 -1
@@ -0,0 +1,424 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { listFiles, downloadFile, deleteFile, type EenFile, type EenError, type ListFilesParams, type FileIncludeField } from 'een-api-toolkit'
4
+
5
+ // Reactive state
6
+ const files = ref<EenFile[]>([])
7
+ const loading = ref(false)
8
+ const error = ref<EenError | null>(null)
9
+ const nextPageToken = ref<string | undefined>(undefined)
10
+ const downloadingId = ref<string | null>(null)
11
+ const deletingId = ref<string | null>(null)
12
+ const selectedFile = ref<EenFile | null>(null)
13
+
14
+ const hasNextPage = computed(() => !!nextPageToken.value)
15
+
16
+ // Request size and createTimestamp by default for better display
17
+ const defaultInclude: FileIncludeField[] = ['size', 'createTimestamp']
18
+
19
+ const params = ref<ListFilesParams>({
20
+ pageSize: 20,
21
+ include: defaultInclude,
22
+ sort: ['-createTimestamp']
23
+ })
24
+
25
+ async function fetchFiles(fetchParams?: ListFilesParams, append = false) {
26
+ loading.value = true
27
+ error.value = null
28
+
29
+ const mergedParams = { ...params.value, ...fetchParams }
30
+ const result = await listFiles(mergedParams)
31
+
32
+ if (result.error) {
33
+ error.value = result.error
34
+ if (!append) {
35
+ files.value = []
36
+ }
37
+ nextPageToken.value = undefined
38
+ } else {
39
+ if (append) {
40
+ files.value = [...files.value, ...result.data.results]
41
+ } else {
42
+ files.value = result.data.results
43
+ }
44
+ nextPageToken.value = result.data.nextPageToken
45
+ }
46
+
47
+ loading.value = false
48
+ return result
49
+ }
50
+
51
+ function refresh() {
52
+ return fetchFiles()
53
+ }
54
+
55
+ async function fetchNextPage() {
56
+ if (!nextPageToken.value) return
57
+ return fetchFiles({ ...params.value, pageToken: nextPageToken.value }, true)
58
+ }
59
+
60
+ async function handleDownload(file: EenFile) {
61
+ downloadingId.value = file.id
62
+ const result = await downloadFile(file.id)
63
+
64
+ if (result.error) {
65
+ error.value = result.error
66
+ } else {
67
+ // Create download link
68
+ const url = URL.createObjectURL(result.data.blob)
69
+ const a = document.createElement('a')
70
+ a.href = url
71
+ a.download = result.data.filename || file.name
72
+ a.click()
73
+ URL.revokeObjectURL(url)
74
+ }
75
+
76
+ downloadingId.value = null
77
+ }
78
+
79
+ async function handleDelete(file: EenFile) {
80
+ if (!confirm(`Are you sure you want to delete "${file.name}"? This will move it to the recycle bin.`)) {
81
+ return
82
+ }
83
+
84
+ deletingId.value = file.id
85
+ const result = await deleteFile(file.id)
86
+
87
+ if (result.error) {
88
+ error.value = result.error
89
+ } else {
90
+ // Remove the file from the list
91
+ files.value = files.value.filter(f => f.id !== file.id)
92
+ }
93
+
94
+ deletingId.value = null
95
+ }
96
+
97
+ function formatDate(timestamp: string | undefined) {
98
+ if (!timestamp) return '-'
99
+ return new Date(timestamp).toLocaleString()
100
+ }
101
+
102
+ function getFileType(file: EenFile) {
103
+ // Use type if available, otherwise derive from mimeType
104
+ if (file.type) return file.type
105
+ if (file.mimeType) {
106
+ if (file.mimeType.startsWith('video/')) return 'video'
107
+ if (file.mimeType.startsWith('image/')) return 'image'
108
+ if (file.mimeType === 'application/directory') return 'folder'
109
+ return file.mimeType.split('/')[1] || file.mimeType
110
+ }
111
+ return '-'
112
+ }
113
+
114
+ function formatSize(bytes: number | undefined) {
115
+ if (bytes === undefined || bytes === null) return '-'
116
+ if (bytes === 0) return '0 B'
117
+ if (bytes < 1024) return `${bytes} B`
118
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
119
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
120
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
121
+ }
122
+
123
+ function openFileDetails(file: EenFile) {
124
+ selectedFile.value = file
125
+ }
126
+
127
+ function closeModal() {
128
+ selectedFile.value = null
129
+ }
130
+
131
+ onMounted(() => {
132
+ fetchFiles()
133
+ })
134
+ </script>
135
+
136
+ <template>
137
+ <div class="files">
138
+ <div class="header">
139
+ <h2>Files</h2>
140
+ <button @click="refresh" :disabled="loading">
141
+ {{ loading ? 'Loading...' : 'Refresh' }}
142
+ </button>
143
+ </div>
144
+
145
+ <div v-if="loading && files.length === 0" class="loading">
146
+ Loading files...
147
+ </div>
148
+
149
+ <div v-else-if="error" class="error">
150
+ Error: {{ error.message }}
151
+ </div>
152
+
153
+ <div v-else>
154
+ <table v-if="files.length > 0">
155
+ <thead>
156
+ <tr>
157
+ <th>Name</th>
158
+ <th>Type</th>
159
+ <th>Size</th>
160
+ <th>Created</th>
161
+ <th>Actions</th>
162
+ </tr>
163
+ </thead>
164
+ <tbody>
165
+ <tr v-for="file in files" :key="file.id">
166
+ <td><a href="#" class="file-link" @click.prevent="openFileDetails(file)">{{ file.name }}</a></td>
167
+ <td>{{ getFileType(file) }}</td>
168
+ <td>{{ formatSize(file.size) }}</td>
169
+ <td>{{ formatDate(file.createTimestamp) }}</td>
170
+ <td class="actions">
171
+ <button
172
+ class="btn-small"
173
+ @click="handleDownload(file)"
174
+ :disabled="downloadingId === file.id || deletingId === file.id"
175
+ >
176
+ {{ downloadingId === file.id ? 'Downloading...' : 'Download' }}
177
+ </button>
178
+ <button
179
+ class="btn-small btn-danger"
180
+ @click="handleDelete(file)"
181
+ :disabled="downloadingId === file.id || deletingId === file.id"
182
+ >
183
+ {{ deletingId === file.id ? 'Deleting...' : 'Delete' }}
184
+ </button>
185
+ </td>
186
+ </tr>
187
+ </tbody>
188
+ </table>
189
+
190
+ <p v-else>No files found.</p>
191
+
192
+ <div v-if="hasNextPage" class="pagination">
193
+ <button @click="fetchNextPage" :disabled="loading">
194
+ {{ loading ? 'Loading...' : 'Load More' }}
195
+ </button>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- File Details Modal -->
200
+ <div v-if="selectedFile" class="modal-overlay" @click.self="closeModal">
201
+ <div class="modal">
202
+ <div class="modal-header">
203
+ <h3>File Details</h3>
204
+ <button class="modal-close" @click="closeModal">&times;</button>
205
+ </div>
206
+ <div class="modal-body">
207
+ <div class="detail-row">
208
+ <span class="detail-label">Name:</span>
209
+ <span class="detail-value">{{ selectedFile.name }}</span>
210
+ </div>
211
+ <div class="detail-row">
212
+ <span class="detail-label">ID:</span>
213
+ <span class="detail-value">{{ selectedFile.id }}</span>
214
+ </div>
215
+ <div class="detail-row">
216
+ <span class="detail-label">Type:</span>
217
+ <span class="detail-value">{{ getFileType(selectedFile) }}</span>
218
+ </div>
219
+ <div v-if="selectedFile.mimeType" class="detail-row">
220
+ <span class="detail-label">MIME Type:</span>
221
+ <span class="detail-value">{{ selectedFile.mimeType }}</span>
222
+ </div>
223
+ <div class="detail-row">
224
+ <span class="detail-label">Size:</span>
225
+ <span class="detail-value">{{ formatSize(selectedFile.size) }}</span>
226
+ </div>
227
+ <div v-if="selectedFile.directory" class="detail-row">
228
+ <span class="detail-label">Directory:</span>
229
+ <span class="detail-value">{{ selectedFile.directory }}</span>
230
+ </div>
231
+ <div class="detail-row">
232
+ <span class="detail-label">Created:</span>
233
+ <span class="detail-value">{{ formatDate(selectedFile.createTimestamp) }}</span>
234
+ </div>
235
+ <div v-if="selectedFile.updateTimestamp" class="detail-row">
236
+ <span class="detail-label">Updated:</span>
237
+ <span class="detail-value">{{ formatDate(selectedFile.updateTimestamp) }}</span>
238
+ </div>
239
+ <div v-if="selectedFile.accountId" class="detail-row">
240
+ <span class="detail-label">Account ID:</span>
241
+ <span class="detail-value">{{ selectedFile.accountId }}</span>
242
+ </div>
243
+ <div v-if="selectedFile.tags && selectedFile.tags.length > 0" class="detail-row">
244
+ <span class="detail-label">Tags:</span>
245
+ <span class="detail-value">{{ selectedFile.tags.join(', ') }}</span>
246
+ </div>
247
+ <div v-if="selectedFile.notes" class="detail-row">
248
+ <span class="detail-label">Notes:</span>
249
+ <span class="detail-value">{{ selectedFile.notes }}</span>
250
+ </div>
251
+ </div>
252
+ <div class="modal-footer">
253
+ <button @click="handleDownload(selectedFile)" :disabled="downloadingId === selectedFile.id">
254
+ {{ downloadingId === selectedFile.id ? 'Downloading...' : 'Download' }}
255
+ </button>
256
+ <button class="btn-secondary" @click="closeModal">Close</button>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </template>
262
+
263
+ <style scoped>
264
+ .files {
265
+ width: 80vw;
266
+ min-width: 900px;
267
+ margin: 0 auto;
268
+ }
269
+
270
+ .header {
271
+ display: flex;
272
+ justify-content: space-between;
273
+ align-items: center;
274
+ margin-bottom: 20px;
275
+ }
276
+
277
+ table {
278
+ width: 100%;
279
+ border-collapse: collapse;
280
+ }
281
+
282
+ th,
283
+ td {
284
+ padding: 12px;
285
+ text-align: left;
286
+ border-bottom: 1px solid #eee;
287
+ }
288
+
289
+ th {
290
+ background: #f5f5f5;
291
+ font-weight: 600;
292
+ }
293
+
294
+ .actions {
295
+ display: flex;
296
+ gap: 8px;
297
+ }
298
+
299
+ .btn-small {
300
+ padding: 5px 10px;
301
+ font-size: 0.85rem;
302
+ }
303
+
304
+ .btn-danger {
305
+ background-color: #e74c3c;
306
+ color: white;
307
+ border: none;
308
+ }
309
+
310
+ .btn-danger:hover:not(:disabled) {
311
+ background-color: #c0392b;
312
+ }
313
+
314
+ .btn-danger:disabled {
315
+ background-color: #f5b7b1;
316
+ cursor: not-allowed;
317
+ }
318
+
319
+ .pagination {
320
+ margin-top: 20px;
321
+ text-align: center;
322
+ }
323
+
324
+ .file-link {
325
+ color: #42b883;
326
+ text-decoration: none;
327
+ cursor: pointer;
328
+ }
329
+
330
+ .file-link:hover {
331
+ text-decoration: underline;
332
+ }
333
+
334
+ /* Modal styles */
335
+ .modal-overlay {
336
+ position: fixed;
337
+ top: 0;
338
+ left: 0;
339
+ right: 0;
340
+ bottom: 0;
341
+ background: rgba(0, 0, 0, 0.5);
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ z-index: 1000;
346
+ }
347
+
348
+ .modal {
349
+ background: white;
350
+ border-radius: 8px;
351
+ width: 70vw;
352
+ max-height: 80vh;
353
+ overflow-y: auto;
354
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
355
+ }
356
+
357
+ .modal-header {
358
+ display: flex;
359
+ justify-content: space-between;
360
+ align-items: center;
361
+ padding: 15px 20px;
362
+ border-bottom: 1px solid #eee;
363
+ }
364
+
365
+ .modal-header h3 {
366
+ margin: 0;
367
+ }
368
+
369
+ .modal-close {
370
+ background: none;
371
+ border: none;
372
+ font-size: 24px;
373
+ cursor: pointer;
374
+ color: #666;
375
+ padding: 0;
376
+ line-height: 1;
377
+ }
378
+
379
+ .modal-close:hover {
380
+ color: #333;
381
+ }
382
+
383
+ .modal-body {
384
+ padding: 20px;
385
+ }
386
+
387
+ .detail-row {
388
+ display: flex;
389
+ margin-bottom: 12px;
390
+ }
391
+
392
+ .detail-row:last-child {
393
+ margin-bottom: 0;
394
+ }
395
+
396
+ .detail-label {
397
+ font-weight: 600;
398
+ width: 30%;
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ .detail-value {
403
+ color: #666;
404
+ word-break: break-all;
405
+ }
406
+
407
+ .modal-footer {
408
+ display: flex;
409
+ justify-content: flex-end;
410
+ gap: 10px;
411
+ padding: 15px 20px;
412
+ border-top: 1px solid #eee;
413
+ }
414
+
415
+ .btn-secondary {
416
+ background: #6c757d;
417
+ color: white;
418
+ border: none;
419
+ }
420
+
421
+ .btn-secondary:hover {
422
+ background: #5a6268;
423
+ }
424
+ </style>
@@ -0,0 +1,195 @@
1
+ <script setup lang="ts">
2
+ import { useAuthStore, getCurrentUser, getAuthUrl, getStorageStrategy, STORAGE_STRATEGY_DESCRIPTIONS, type UserProfile, type EenError } from 'een-api-toolkit'
3
+ import { computed, ref, watch } from 'vue'
4
+
5
+ const authStore = useAuthStore()
6
+ const isAuthenticated = computed(() => authStore.isAuthenticated)
7
+ const loginError = ref<string | null>(null)
8
+
9
+ const storageStrategy = getStorageStrategy()
10
+ const storageDescription = STORAGE_STRATEGY_DESCRIPTIONS[storageStrategy]
11
+
12
+ function login() {
13
+ try {
14
+ loginError.value = null
15
+ const authUrl = getAuthUrl()
16
+ window.location.href = authUrl
17
+ } catch (err) {
18
+ loginError.value = err instanceof Error ? err.message : 'Failed to initiate login'
19
+ console.error('Login error:', err)
20
+ }
21
+ }
22
+
23
+ // Reactive state for current user
24
+ const user = ref<UserProfile | null>(null)
25
+ const loading = ref(false)
26
+ const error = ref<EenError | null>(null)
27
+
28
+ // Guard to prevent concurrent fetch calls
29
+ let fetchInProgress = false
30
+
31
+ async function fetchUser() {
32
+ if (fetchInProgress) return
33
+ fetchInProgress = true
34
+ loading.value = true
35
+ error.value = null
36
+
37
+ try {
38
+ const result = await getCurrentUser()
39
+ if (result.error) {
40
+ error.value = result.error
41
+ user.value = null
42
+ } else {
43
+ user.value = result.data
44
+ }
45
+ } finally {
46
+ loading.value = false
47
+ fetchInProgress = false
48
+ }
49
+ }
50
+
51
+ // Fetch user when authentication state changes
52
+ watch(
53
+ isAuthenticated,
54
+ async (isAuth) => {
55
+ if (isAuth && !user.value && !fetchInProgress) {
56
+ await fetchUser()
57
+ }
58
+ },
59
+ { immediate: true }
60
+ )
61
+ </script>
62
+
63
+ <template>
64
+ <div class="home">
65
+ <h2>Welcome to the EEN Jobs Example</h2>
66
+
67
+ <div v-if="!isAuthenticated" class="not-authenticated" data-testid="not-authenticated">
68
+ <p data-testid="not-authenticated-message">You are not logged in.</p>
69
+ <p v-if="loginError" class="error" data-testid="login-error">{{ loginError }}</p>
70
+ <button data-testid="login-button" @click="login">Login with Eagle Eye Networks</button>
71
+ </div>
72
+
73
+ <div v-else class="authenticated">
74
+ <div v-if="loading" class="loading">Loading user profile...</div>
75
+ <div v-else-if="error" class="error">Error: {{ error.message }}</div>
76
+ <div v-else-if="user" class="user-info">
77
+ <h3>Hello, {{ user.firstName }} {{ user.lastName }}!</h3>
78
+ <p>Email: {{ user.email }}</p>
79
+ <p>Account ID: {{ user.accountId }}</p>
80
+ </div>
81
+
82
+ <div class="actions">
83
+ <router-link to="/jobs">
84
+ <button>View Jobs</button>
85
+ </router-link>
86
+ <router-link to="/files">
87
+ <button>View Files</button>
88
+ </router-link>
89
+ <router-link to="/create-export">
90
+ <button>Create Export</button>
91
+ </router-link>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="description">
96
+ <h3>About This Example</h3>
97
+ <p>
98
+ This example demonstrates how to use the Jobs, Files, and Exports APIs from
99
+ the EEN API Toolkit to manage video exports and downloadable files from the
100
+ Eagle Eye Networks platform.
101
+ </p>
102
+ <h4>Features</h4>
103
+ <ul>
104
+ <li>List jobs with state filtering</li>
105
+ <li>View job progress and poll for completion</li>
106
+ <li>Create video exports from cameras</li>
107
+ <li>List and download files</li>
108
+ <li>OAuth authentication flow</li>
109
+ </ul>
110
+ <p class="storage-note" data-testid="storage-strategy">
111
+ Storage strategy: <strong>{{ storageStrategy }}</strong> ({{ storageDescription }})
112
+ </p>
113
+ </div>
114
+ </div>
115
+ </template>
116
+
117
+ <style scoped>
118
+ .home {
119
+ text-align: center;
120
+ }
121
+
122
+ h2 {
123
+ margin-bottom: 30px;
124
+ }
125
+
126
+ .not-authenticated,
127
+ .authenticated {
128
+ margin-top: 20px;
129
+ }
130
+
131
+ .user-info {
132
+ background: #f5f5f5;
133
+ padding: 20px;
134
+ border-radius: 8px;
135
+ margin-bottom: 20px;
136
+ }
137
+
138
+ .user-info h3 {
139
+ margin-bottom: 10px;
140
+ }
141
+
142
+ .user-info p {
143
+ color: #666;
144
+ }
145
+
146
+ .actions {
147
+ margin-top: 20px;
148
+ display: flex;
149
+ gap: 10px;
150
+ justify-content: center;
151
+ }
152
+
153
+ .description {
154
+ margin-top: 40px;
155
+ padding: 20px;
156
+ background: #f8f9fa;
157
+ border-radius: 8px;
158
+ text-align: left;
159
+ }
160
+
161
+ .description h3 {
162
+ margin-bottom: 10px;
163
+ }
164
+
165
+ .description h4 {
166
+ margin-top: 15px;
167
+ margin-bottom: 10px;
168
+ }
169
+
170
+ .description p {
171
+ color: #666;
172
+ margin-bottom: 15px;
173
+ }
174
+
175
+ .description ul {
176
+ list-style: disc;
177
+ padding-left: 20px;
178
+ color: #666;
179
+ }
180
+
181
+ .description code {
182
+ background: #e9ecef;
183
+ padding: 2px 6px;
184
+ border-radius: 3px;
185
+ font-size: 0.9em;
186
+ }
187
+
188
+ .storage-note {
189
+ margin-top: 20px;
190
+ padding-top: 15px;
191
+ border-top: 1px solid #ddd;
192
+ font-size: 0.85em;
193
+ color: #888;
194
+ }
195
+ </style>