een-api-toolkit 0.3.13 → 0.3.15

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 (37) hide show
  1. package/CHANGELOG.md +5 -35
  2. package/README.md +2 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +801 -0
  6. package/dist/index.js +486 -252
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +195 -2
  9. package/examples/vue-alerts-metrics/README.md +136 -0
  10. package/examples/vue-alerts-metrics/e2e/app.spec.ts +74 -0
  11. package/examples/vue-alerts-metrics/e2e/auth.spec.ts +554 -0
  12. package/examples/vue-alerts-metrics/index.html +13 -0
  13. package/examples/vue-alerts-metrics/package-lock.json +1749 -0
  14. package/examples/vue-alerts-metrics/package.json +30 -0
  15. package/examples/vue-alerts-metrics/playwright.config.ts +46 -0
  16. package/examples/vue-alerts-metrics/src/App.vue +108 -0
  17. package/examples/vue-alerts-metrics/src/components/AlertsList.vue +330 -0
  18. package/examples/vue-alerts-metrics/src/components/CameraSelector.vue +96 -0
  19. package/examples/vue-alerts-metrics/src/components/MetricsChart.vue +322 -0
  20. package/examples/vue-alerts-metrics/src/components/NotificationsList.vue +263 -0
  21. package/examples/vue-alerts-metrics/src/components/TimeRangeSelector.vue +74 -0
  22. package/examples/vue-alerts-metrics/src/main.ts +23 -0
  23. package/examples/vue-alerts-metrics/src/router/index.ts +61 -0
  24. package/examples/vue-alerts-metrics/src/views/Callback.vue +76 -0
  25. package/examples/vue-alerts-metrics/src/views/Dashboard.vue +152 -0
  26. package/examples/vue-alerts-metrics/src/views/Home.vue +167 -0
  27. package/examples/vue-alerts-metrics/src/views/Login.vue +33 -0
  28. package/examples/vue-alerts-metrics/src/views/Logout.vue +66 -0
  29. package/examples/vue-alerts-metrics/src/vite-env.d.ts +12 -0
  30. package/examples/vue-alerts-metrics/tsconfig.json +21 -0
  31. package/examples/vue-alerts-metrics/tsconfig.node.json +10 -0
  32. package/examples/vue-alerts-metrics/vite.config.ts +12 -0
  33. package/examples/vue-events/README.md +68 -0
  34. package/examples/vue-events/e2e/auth.spec.ts +105 -0
  35. package/examples/vue-events/src/components/EventsModal.vue +452 -14
  36. package/examples/vue-events/src/views/Home.vue +1 -0
  37. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "een-api-toolkit-alerts-metrics-example",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "stop": "lsof -ti :3333 2>/dev/null | xargs -r kill -9 || echo 'Port 3333 is free'",
8
+ "dev": "npm run stop && vite",
9
+ "build": "vue-tsc && vite build",
10
+ "preview": "vite preview",
11
+ "test:e2e": "playwright test",
12
+ "test:e2e:ui": "playwright test --ui"
13
+ },
14
+ "dependencies": {
15
+ "een-api-toolkit": "file:../..",
16
+ "pinia": "^3.0.4",
17
+ "vue": "^3.4.0",
18
+ "vue-router": "^4.2.0",
19
+ "chart.js": "^4.4.0",
20
+ "vue-chartjs": "^5.3.0"
21
+ },
22
+ "devDependencies": {
23
+ "@playwright/test": "^1.57.0",
24
+ "@vitejs/plugin-vue": "^6.0.0",
25
+ "dotenv": "^17.2.3",
26
+ "typescript": "~5.8.0",
27
+ "vite": "^7.3.0",
28
+ "vue-tsc": "^3.2.1"
29
+ }
30
+ }
@@ -0,0 +1,46 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+ import dotenv from 'dotenv'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ // Load .env files: parent first, then local with override to replace any conflicts
9
+ dotenv.config({ path: path.resolve(__dirname, '../../.env') })
10
+ dotenv.config({ path: path.resolve(__dirname, '.env'), override: true })
11
+
12
+ const redirectUri = process.env.VITE_REDIRECT_URI || 'http://127.0.0.1:3333'
13
+ if (!redirectUri.startsWith('http://127.0.0.1:') && !redirectUri.startsWith('http://localhost:')) {
14
+ throw new Error('VITE_REDIRECT_URI must use localhost or 127.0.0.1 for security')
15
+ }
16
+ const baseURL = redirectUri
17
+
18
+ export default defineConfig({
19
+ testDir: './e2e',
20
+ testMatch: '**/*.spec.ts',
21
+ fullyParallel: false,
22
+ forbidOnly: !!process.env.CI,
23
+ retries: 0,
24
+ maxFailures: 1,
25
+ workers: 1,
26
+ reporter: [['html', { open: 'never' }]],
27
+ timeout: 30000,
28
+ use: {
29
+ baseURL,
30
+ trace: 'on-first-retry',
31
+ video: 'retain-on-failure'
32
+ },
33
+ outputDir: './e2e-results/',
34
+ projects: [
35
+ {
36
+ name: 'chromium',
37
+ use: { ...devices['Desktop Chrome'] }
38
+ }
39
+ ],
40
+ webServer: {
41
+ command: 'npm run dev',
42
+ url: baseURL,
43
+ reuseExistingServer: !process.env.CI,
44
+ timeout: 30000
45
+ }
46
+ })
@@ -0,0 +1,108 @@
1
+ <script setup lang="ts">
2
+ import { useAuthStore } from 'een-api-toolkit'
3
+ import { computed } from 'vue'
4
+
5
+ const authStore = useAuthStore()
6
+ const isAuthenticated = computed(() => authStore.isAuthenticated)
7
+ </script>
8
+
9
+ <template>
10
+ <div class="app">
11
+ <header>
12
+ <h1>EEN Alerts & Metrics Example</h1>
13
+ <nav>
14
+ <router-link to="/" data-testid="nav-home">Home</router-link>
15
+ <router-link v-if="isAuthenticated" to="/dashboard" data-testid="nav-dashboard">Dashboard</router-link>
16
+ <router-link v-if="!isAuthenticated" to="/login" data-testid="nav-login">Login</router-link>
17
+ <router-link v-if="isAuthenticated" to="/logout" data-testid="nav-logout">Logout</router-link>
18
+ </nav>
19
+ </header>
20
+ <main>
21
+ <router-view />
22
+ </main>
23
+ </div>
24
+ </template>
25
+
26
+ <style>
27
+ * {
28
+ box-sizing: border-box;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
35
+ Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
36
+ line-height: 1.6;
37
+ color: #333;
38
+ }
39
+
40
+ .app {
41
+ max-width: 1400px;
42
+ margin: 0 auto;
43
+ padding: 20px;
44
+ }
45
+
46
+ header {
47
+ display: flex;
48
+ justify-content: space-between;
49
+ align-items: center;
50
+ margin-bottom: 30px;
51
+ padding-bottom: 20px;
52
+ border-bottom: 1px solid #eee;
53
+ }
54
+
55
+ header h1 {
56
+ font-size: 1.5rem;
57
+ }
58
+
59
+ nav {
60
+ display: flex;
61
+ gap: 20px;
62
+ }
63
+
64
+ nav a {
65
+ color: #42b883;
66
+ text-decoration: none;
67
+ }
68
+
69
+ nav a:hover {
70
+ text-decoration: underline;
71
+ }
72
+
73
+ nav a.router-link-active {
74
+ font-weight: bold;
75
+ }
76
+
77
+ button {
78
+ background: #42b883;
79
+ color: white;
80
+ border: none;
81
+ padding: 10px 20px;
82
+ border-radius: 4px;
83
+ cursor: pointer;
84
+ font-size: 1rem;
85
+ }
86
+
87
+ button:hover {
88
+ background: #3aa876;
89
+ }
90
+
91
+ button:disabled {
92
+ background: #ccc;
93
+ cursor: not-allowed;
94
+ }
95
+
96
+ .error {
97
+ color: #e74c3c;
98
+ padding: 10px;
99
+ background: #fdf2f2;
100
+ border-radius: 4px;
101
+ margin: 10px 0;
102
+ }
103
+
104
+ .loading {
105
+ color: #666;
106
+ font-style: italic;
107
+ }
108
+ </style>
@@ -0,0 +1,330 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+ import { listAlerts, listAlertTypes, type Camera, type Alert, type AlertType, type EenError } from 'een-api-toolkit'
4
+
5
+ const props = defineProps<{
6
+ camera: Camera
7
+ timeRange: string
8
+ }>()
9
+
10
+ const alerts = ref<Alert[]>([])
11
+ const alertTypes = ref<AlertType[]>([])
12
+ const selectedAlertType = ref<string>('')
13
+ const loadingAlertTypes = ref(false)
14
+ const loading = ref(false)
15
+ const loadingMore = ref(false)
16
+ const error = ref<EenError | null>(null)
17
+ const nextPageToken = ref<string | undefined>(undefined)
18
+
19
+ function getTimeRangeMs(range: string): number {
20
+ switch (range) {
21
+ case '1h': return 60 * 60 * 1000
22
+ case '6h': return 6 * 60 * 60 * 1000
23
+ case '24h': return 24 * 60 * 60 * 1000
24
+ case '7d': return 7 * 24 * 60 * 60 * 1000
25
+ default: return 24 * 60 * 60 * 1000
26
+ }
27
+ }
28
+
29
+ function formatAlertType(type: string): string {
30
+ // Convert "een.motionDetectionAlert.v1" to "Motion Detection Alert"
31
+ const parts = type.split('.')
32
+ if (parts.length >= 2) {
33
+ const name = parts[1]
34
+ .replace(/([A-Z])/g, ' $1')
35
+ .trim()
36
+ return name.charAt(0).toUpperCase() + name.slice(1)
37
+ }
38
+ return type
39
+ }
40
+
41
+ async function fetchAlertTypes() {
42
+ loadingAlertTypes.value = true
43
+ error.value = null
44
+ alertTypes.value = []
45
+ selectedAlertType.value = ''
46
+
47
+ const result = await listAlertTypes({ pageSize: 100 })
48
+
49
+ if (result.error) {
50
+ error.value = result.error
51
+ loadingAlertTypes.value = false
52
+ return
53
+ }
54
+
55
+ alertTypes.value = result.data?.results ?? []
56
+ loadingAlertTypes.value = false
57
+ }
58
+
59
+ async function fetchAlerts(append = false) {
60
+ if (!props.camera?.id) return
61
+
62
+ if (append) {
63
+ loadingMore.value = true
64
+ } else {
65
+ loading.value = true
66
+ alerts.value = []
67
+ nextPageToken.value = undefined
68
+ }
69
+ error.value = null
70
+
71
+ const now = new Date()
72
+ const rangeMs = getTimeRangeMs(props.timeRange)
73
+ const startTime = new Date(now.getTime() - rangeMs)
74
+
75
+ const params: Parameters<typeof listAlerts>[0] = {
76
+ actorId__in: [props.camera.id],
77
+ timestamp__gte: startTime.toISOString(),
78
+ timestamp__lte: now.toISOString(),
79
+ pageSize: 20,
80
+ pageToken: append ? nextPageToken.value : undefined,
81
+ include: ['description'],
82
+ sort: ['-timestamp']
83
+ }
84
+
85
+ // Add alert type filter if selected
86
+ if (selectedAlertType.value) {
87
+ params.alertType__in = [selectedAlertType.value]
88
+ }
89
+
90
+ const result = await listAlerts(params)
91
+
92
+ if (result.error) {
93
+ error.value = result.error
94
+ } else {
95
+ const newAlerts = result.data?.results ?? []
96
+ if (append) {
97
+ alerts.value = [...alerts.value, ...newAlerts]
98
+ } else {
99
+ alerts.value = newAlerts
100
+ }
101
+ nextPageToken.value = result.data?.nextPageToken
102
+ }
103
+
104
+ loading.value = false
105
+ loadingMore.value = false
106
+ }
107
+
108
+ function handleAlertTypeChange() {
109
+ fetchAlerts()
110
+ }
111
+
112
+ function loadMore() {
113
+ if (nextPageToken.value) {
114
+ fetchAlerts(true)
115
+ }
116
+ }
117
+
118
+ function formatTime(timestamp: string): string {
119
+ return new Date(timestamp).toLocaleString()
120
+ }
121
+
122
+ function getPriorityClass(priority?: number): string {
123
+ if (priority === undefined) return ''
124
+ if (priority >= 15) return 'priority-high'
125
+ if (priority >= 10) return 'priority-medium'
126
+ return 'priority-low'
127
+ }
128
+
129
+ // Fetch alert types once on mount
130
+ fetchAlertTypes()
131
+
132
+ // Fetch alerts when camera or time range changes
133
+ watch(
134
+ () => [props.camera?.id, props.timeRange],
135
+ () => {
136
+ fetchAlerts()
137
+ },
138
+ { immediate: true }
139
+ )
140
+ </script>
141
+
142
+ <template>
143
+ <div class="alerts-list" data-testid="alerts-list">
144
+ <div class="alert-type-selector" data-testid="alert-type-selector">
145
+ <label for="alert-type-select">Alert Type:</label>
146
+ <select
147
+ id="alert-type-select"
148
+ v-model="selectedAlertType"
149
+ @change="handleAlertTypeChange"
150
+ :disabled="loadingAlertTypes"
151
+ data-testid="alert-type-select"
152
+ >
153
+ <option value="">
154
+ {{ loadingAlertTypes ? 'Loading...' : 'All Alert Types' }}
155
+ </option>
156
+ <option
157
+ v-for="at in alertTypes"
158
+ :key="at.type"
159
+ :value="at.type"
160
+ data-testid="alert-type-option"
161
+ >
162
+ {{ formatAlertType(at.type) }}
163
+ </option>
164
+ </select>
165
+ </div>
166
+
167
+ <div v-if="loading" class="loading" data-testid="alerts-loading">
168
+ Loading alerts...
169
+ </div>
170
+ <div v-else-if="error" class="error" data-testid="alerts-error">
171
+ {{ error.message }}
172
+ </div>
173
+ <div v-else-if="alerts.length === 0" class="no-data" data-testid="alerts-no-data">
174
+ No alerts found for this time range.
175
+ </div>
176
+ <div v-else>
177
+ <div
178
+ v-for="alert in alerts"
179
+ :key="alert.id"
180
+ class="alert-item"
181
+ data-testid="alert-item"
182
+ >
183
+ <div class="alert-header">
184
+ <span class="alert-type">{{ alert.alertType?.split('.')[1] || alert.alertType }}</span>
185
+ <span
186
+ v-if="alert.priority !== undefined"
187
+ class="priority-badge"
188
+ :class="getPriorityClass(alert.priority)"
189
+ >
190
+ P{{ alert.priority }}
191
+ </span>
192
+ </div>
193
+ <div class="alert-time">{{ formatTime(alert.timestamp) }}</div>
194
+ <div v-if="alert.description" class="alert-description">
195
+ {{ alert.description }}
196
+ </div>
197
+ </div>
198
+
199
+ <button
200
+ v-if="nextPageToken"
201
+ @click="loadMore"
202
+ :disabled="loadingMore"
203
+ class="load-more-button"
204
+ data-testid="alerts-load-more"
205
+ >
206
+ {{ loadingMore ? 'Loading...' : 'Load More' }}
207
+ </button>
208
+ </div>
209
+ </div>
210
+ </template>
211
+
212
+ <style scoped>
213
+ .alerts-list {
214
+ min-height: 200px;
215
+ }
216
+
217
+ .alert-type-selector {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 10px;
221
+ margin-bottom: 15px;
222
+ }
223
+
224
+ .alert-type-selector label {
225
+ font-weight: 500;
226
+ font-size: 0.9rem;
227
+ }
228
+
229
+ .alert-type-selector select {
230
+ padding: 6px 10px;
231
+ border: 1px solid #ddd;
232
+ border-radius: 4px;
233
+ font-size: 0.85rem;
234
+ min-width: 200px;
235
+ }
236
+
237
+ .alert-type-selector select:disabled {
238
+ background: #f5f5f5;
239
+ cursor: not-allowed;
240
+ }
241
+
242
+ .loading,
243
+ .no-data {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ height: 100px;
248
+ color: #666;
249
+ font-style: italic;
250
+ }
251
+
252
+ .error {
253
+ color: #e74c3c;
254
+ padding: 10px;
255
+ background: #fdf2f2;
256
+ border-radius: 4px;
257
+ }
258
+
259
+ .alert-item {
260
+ padding: 12px;
261
+ border: 1px solid #eee;
262
+ border-radius: 6px;
263
+ margin-bottom: 10px;
264
+ background: #fafafa;
265
+ }
266
+
267
+ .alert-header {
268
+ display: flex;
269
+ justify-content: space-between;
270
+ align-items: center;
271
+ margin-bottom: 5px;
272
+ }
273
+
274
+ .alert-type {
275
+ font-weight: 600;
276
+ color: #333;
277
+ font-size: 0.9rem;
278
+ }
279
+
280
+ .priority-badge {
281
+ padding: 2px 8px;
282
+ border-radius: 12px;
283
+ font-size: 0.75rem;
284
+ font-weight: 500;
285
+ }
286
+
287
+ .priority-high {
288
+ background: #fde8e8;
289
+ color: #c53030;
290
+ }
291
+
292
+ .priority-medium {
293
+ background: #fef3c7;
294
+ color: #92400e;
295
+ }
296
+
297
+ .priority-low {
298
+ background: #def7ec;
299
+ color: #03543f;
300
+ }
301
+
302
+ .alert-time {
303
+ font-size: 0.8rem;
304
+ color: #666;
305
+ margin-bottom: 5px;
306
+ }
307
+
308
+ .alert-description {
309
+ font-size: 0.85rem;
310
+ color: #555;
311
+ }
312
+
313
+ .load-more-button {
314
+ width: 100%;
315
+ margin-top: 10px;
316
+ padding: 10px;
317
+ background: #f5f5f5;
318
+ border: 1px solid #ddd;
319
+ color: #333;
320
+ }
321
+
322
+ .load-more-button:hover {
323
+ background: #eee;
324
+ }
325
+
326
+ .load-more-button:disabled {
327
+ background: #f5f5f5;
328
+ color: #999;
329
+ }
330
+ </style>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { getCameras, type Camera, type EenError } from 'een-api-toolkit'
4
+
5
+ const emit = defineEmits<{
6
+ select: [camera: Camera]
7
+ }>()
8
+
9
+ const cameras = ref<Camera[]>([])
10
+ const loading = ref(false)
11
+ const error = ref<EenError | null>(null)
12
+ const selectedCameraId = ref<string>('')
13
+
14
+ async function fetchCameras() {
15
+ loading.value = true
16
+ error.value = null
17
+
18
+ const result = await getCameras({ pageSize: 100 })
19
+
20
+ if (result.error) {
21
+ error.value = result.error
22
+ cameras.value = []
23
+ } else {
24
+ cameras.value = result.data?.results ?? []
25
+ }
26
+
27
+ loading.value = false
28
+ }
29
+
30
+ function handleChange() {
31
+ const camera = cameras.value.find(c => c.id === selectedCameraId.value)
32
+ if (camera) {
33
+ emit('select', camera)
34
+ }
35
+ }
36
+
37
+ onMounted(() => {
38
+ fetchCameras()
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <div class="camera-selector">
44
+ <label for="camera-select">Camera:</label>
45
+ <select
46
+ id="camera-select"
47
+ v-model="selectedCameraId"
48
+ @change="handleChange"
49
+ :disabled="loading"
50
+ data-testid="camera-select"
51
+ >
52
+ <option value="" disabled>{{ loading ? 'Loading cameras...' : 'Select a camera' }}</option>
53
+ <option
54
+ v-for="camera in cameras"
55
+ :key="camera.id"
56
+ :value="camera.id"
57
+ data-testid="camera-option"
58
+ >
59
+ {{ camera.name ? `${camera.name} (${camera.id})` : camera.id }}
60
+ </option>
61
+ </select>
62
+ <div v-if="error" class="error" data-testid="camera-selector-error">
63
+ {{ error.message }}
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <style scoped>
69
+ .camera-selector {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 10px;
73
+ }
74
+
75
+ label {
76
+ font-weight: 500;
77
+ }
78
+
79
+ select {
80
+ padding: 8px 12px;
81
+ border: 1px solid #ddd;
82
+ border-radius: 4px;
83
+ font-size: 1rem;
84
+ min-width: 350px;
85
+ }
86
+
87
+ select:disabled {
88
+ background: #f5f5f5;
89
+ cursor: not-allowed;
90
+ }
91
+
92
+ .error {
93
+ color: #e74c3c;
94
+ font-size: 0.9rem;
95
+ }
96
+ </style>